feat: rewrite skill creator skill in ts and bundle (#333)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@ bun.lockb
|
|||||||
bin/
|
bin/
|
||||||
letta.js
|
letta.js
|
||||||
letta.js.map
|
letta.js.map
|
||||||
|
/skills/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
|
|||||||
@@ -1,202 +0,0 @@
|
|||||||
|
|
||||||
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.
|
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Skill Initializer - Creates a new skill from template
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
init_skill.py <skill-name> --path <path>
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
init_skill.py my-new-skill --path skills/public
|
|
||||||
init_skill.py my-api-helper --path skills/private
|
|
||||||
init_skill.py custom-skill --path /custom/location
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from pathlib import 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.]
|
|
||||||
---
|
|
||||||
|
|
||||||
# {skill_title}
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
[TODO: 1-2 sentences explaining what this skill enables]
|
|
||||||
|
|
||||||
## Structuring This Skill
|
|
||||||
|
|
||||||
[TODO: Choose the structure that best fits this skill's purpose. Common patterns:
|
|
||||||
|
|
||||||
**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...
|
|
||||||
|
|
||||||
**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...
|
|
||||||
|
|
||||||
**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...
|
|
||||||
|
|
||||||
**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...
|
|
||||||
|
|
||||||
Patterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations).
|
|
||||||
|
|
||||||
Delete this entire "Structuring This Skill" section when done - it's just guidance.]
|
|
||||||
|
|
||||||
## [TODO: Replace with the first main section based on chosen structure]
|
|
||||||
|
|
||||||
[TODO: Add content here. See examples in existing skills:
|
|
||||||
- Code samples for technical skills
|
|
||||||
- Decision trees for complex workflows
|
|
||||||
- Concrete examples with realistic user requests
|
|
||||||
- References to scripts/templates/references as needed]
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
This skill includes example resource directories that demonstrate how to organize different types of bundled resources:
|
|
||||||
|
|
||||||
### scripts/
|
|
||||||
Executable code (Python/Bash/etc.) that can be run directly to perform specific operations.
|
|
||||||
|
|
||||||
**Examples from other skills:**
|
|
||||||
- PDF skill: `fill_fillable_fields.py`, `extract_form_field_info.py` - utilities for PDF manipulation
|
|
||||||
- DOCX skill: `document.py`, `utilities.py` - Python modules for document processing
|
|
||||||
|
|
||||||
**Appropriate for:** Python scripts, shell scripts, or any executable code that performs automation, data processing, or specific operations.
|
|
||||||
|
|
||||||
**Note:** Scripts may be executed without loading into context, but can still be read by the Letta Code agent for patching or environment adjustments.
|
|
||||||
|
|
||||||
### references/
|
|
||||||
Documentation and reference material intended to be loaded into context to inform the Letta Code agent's process and thinking.
|
|
||||||
|
|
||||||
**Examples from other skills:**
|
|
||||||
- Product management: `communication.md`, `context_building.md` - detailed workflow guides
|
|
||||||
- BigQuery: API reference documentation and query examples
|
|
||||||
- Finance: Schema documentation, company policies
|
|
||||||
|
|
||||||
**Appropriate for:** In-depth documentation, API references, database schemas, comprehensive guides, or any detailed information that the Letta Code agent should reference while working.
|
|
||||||
|
|
||||||
### assets/
|
|
||||||
Files not intended to be loaded into context, but rather used within the output the Letta Code agent produces.
|
|
||||||
|
|
||||||
**Examples from other skills:**
|
|
||||||
- Brand styling: PowerPoint template files (.pptx), logo files
|
|
||||||
- Frontend builder: HTML/React boilerplate project directories
|
|
||||||
- Typography: Font files (.ttf, .woff2)
|
|
||||||
|
|
||||||
**Appropriate for:** Templates, boilerplate code, document templates, images, icons, fonts, or any files meant to be copied or used in the final output.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Any unneeded directories can be deleted.** Not every skill requires all three types of resources.
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLE_SCRIPT = '''#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Example helper script for {skill_name}
|
|
||||||
|
|
||||||
This is a placeholder script that can be executed directly.
|
|
||||||
Replace with actual implementation or delete if not needed.
|
|
||||||
|
|
||||||
Example real scripts from other skills:
|
|
||||||
- pdf/scripts/fill_fillable_fields.py - Fills PDF form fields
|
|
||||||
- pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images
|
|
||||||
"""
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("This is an example script for {skill_name}")
|
|
||||||
# TODO: Add actual script logic here
|
|
||||||
# This could be data processing, file conversion, API calls, etc.
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
'''
|
|
||||||
|
|
||||||
EXAMPLE_REFERENCE = """# Reference Documentation for {skill_title}
|
|
||||||
|
|
||||||
This is a placeholder for detailed reference documentation.
|
|
||||||
Replace with actual reference content or delete if not needed.
|
|
||||||
|
|
||||||
Example real reference docs from other skills:
|
|
||||||
- product-management/references/communication.md - Comprehensive guide for status updates
|
|
||||||
- product-management/references/context_building.md - Deep-dive on gathering context
|
|
||||||
- bigquery/references/ - API references and query examples
|
|
||||||
|
|
||||||
## When Reference Docs Are Useful
|
|
||||||
|
|
||||||
Reference docs are ideal for:
|
|
||||||
- Comprehensive API documentation
|
|
||||||
- Detailed workflow guides
|
|
||||||
- Complex multi-step processes
|
|
||||||
- Information too lengthy for main SKILL.md
|
|
||||||
- Content that's only needed for specific use cases
|
|
||||||
|
|
||||||
## Structure Suggestions
|
|
||||||
|
|
||||||
### API Reference Example
|
|
||||||
- Overview
|
|
||||||
- Authentication
|
|
||||||
- Endpoints with examples
|
|
||||||
- Error codes
|
|
||||||
- Rate limits
|
|
||||||
|
|
||||||
### Workflow Guide Example
|
|
||||||
- Prerequisites
|
|
||||||
- Step-by-step instructions
|
|
||||||
- Common patterns
|
|
||||||
- Troubleshooting
|
|
||||||
- Best practices
|
|
||||||
"""
|
|
||||||
|
|
||||||
EXAMPLE_ASSET = """# Example Asset File
|
|
||||||
|
|
||||||
This placeholder represents where asset files would be stored.
|
|
||||||
Replace with actual asset files (templates, images, fonts, etc.) or delete if not needed.
|
|
||||||
|
|
||||||
Asset files are NOT intended to be loaded into context, but rather used within
|
|
||||||
the output the Letta Code agent produces.
|
|
||||||
|
|
||||||
Example asset files from other skills:
|
|
||||||
- Brand guidelines: logo.png, slides_template.pptx
|
|
||||||
- Frontend builder: hello-world/ directory with HTML/React boilerplate
|
|
||||||
- Typography: custom-font.ttf, font-family.woff2
|
|
||||||
- Data: sample_data.csv, test_dataset.json
|
|
||||||
|
|
||||||
## Common Asset Types
|
|
||||||
|
|
||||||
- Templates: .pptx, .docx, boilerplate directories
|
|
||||||
- Images: .png, .jpg, .svg, .gif
|
|
||||||
- Fonts: .ttf, .otf, .woff, .woff2
|
|
||||||
- Boilerplate code: Project directories, starter files
|
|
||||||
- Icons: .ico, .svg
|
|
||||||
- Data files: .csv, .json, .xml, .yaml
|
|
||||||
|
|
||||||
Note: This is a text placeholder. Actual assets can be any file type.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def title_case_skill_name(skill_name):
|
|
||||||
"""Convert hyphenated skill name to Title Case for display."""
|
|
||||||
return ' '.join(word.capitalize() for word in skill_name.split('-'))
|
|
||||||
|
|
||||||
|
|
||||||
def init_skill(skill_name, path):
|
|
||||||
"""
|
|
||||||
Initialize a new skill directory with template SKILL.md.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
skill_name: Name of the skill
|
|
||||||
path: Path where the skill directory should be created
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Path to created skill directory, or None if error
|
|
||||||
"""
|
|
||||||
# Determine skill directory path
|
|
||||||
skill_dir = Path(path).resolve() / skill_name
|
|
||||||
|
|
||||||
# Check if directory already exists
|
|
||||||
if skill_dir.exists():
|
|
||||||
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}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error creating directory: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Create SKILL.md from template
|
|
||||||
skill_title = title_case_skill_name(skill_name)
|
|
||||||
skill_content = SKILL_TEMPLATE.format(
|
|
||||||
skill_name=skill_name,
|
|
||||||
skill_title=skill_title
|
|
||||||
)
|
|
||||||
|
|
||||||
skill_md_path = skill_dir / 'SKILL.md'
|
|
||||||
try:
|
|
||||||
skill_md_path.write_text(skill_content)
|
|
||||||
print("✅ Created SKILL.md")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error creating SKILL.md: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Create resource directories with example files
|
|
||||||
try:
|
|
||||||
# Create scripts/ directory with example script
|
|
||||||
scripts_dir = skill_dir / 'scripts'
|
|
||||||
scripts_dir.mkdir(exist_ok=True)
|
|
||||||
example_script = scripts_dir / 'example.py'
|
|
||||||
example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name))
|
|
||||||
example_script.chmod(0o755)
|
|
||||||
print("✅ Created scripts/example.py")
|
|
||||||
|
|
||||||
# Create references/ directory with example reference doc
|
|
||||||
references_dir = skill_dir / 'references'
|
|
||||||
references_dir.mkdir(exist_ok=True)
|
|
||||||
example_reference = references_dir / 'api_reference.md'
|
|
||||||
example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title))
|
|
||||||
print("✅ Created references/api_reference.md")
|
|
||||||
|
|
||||||
# Create assets/ directory with example asset placeholder
|
|
||||||
assets_dir = skill_dir / 'assets'
|
|
||||||
assets_dir.mkdir(exist_ok=True)
|
|
||||||
example_asset = assets_dir / 'example_asset.txt'
|
|
||||||
example_asset.write_text(EXAMPLE_ASSET)
|
|
||||||
print("✅ Created assets/example_asset.txt")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error creating resource directories: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Print next steps
|
|
||||||
print(f"\n✅ 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")
|
|
||||||
print("2. Customize or delete the example files in scripts/, references/, and assets/")
|
|
||||||
print("3. Run the validator when ready to check the skill structure")
|
|
||||||
|
|
||||||
return skill_dir
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
if len(sys.argv) < 4 or sys.argv[2] != '--path':
|
|
||||||
print("Usage: init_skill.py <skill-name> --path <path>")
|
|
||||||
print("\nSkill name requirements:")
|
|
||||||
print(" - Hyphen-case identifier (e.g., 'data-analyzer')")
|
|
||||||
print(" - Lowercase letters, digits, and hyphens only")
|
|
||||||
print(" - Max 40 characters")
|
|
||||||
print(" - Must match directory name exactly")
|
|
||||||
print("\nExamples:")
|
|
||||||
print(" init_skill.py my-new-skill --path skills/public")
|
|
||||||
print(" init_skill.py my-api-helper --path skills/private")
|
|
||||||
print(" init_skill.py custom-skill --path /custom/location")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
skill_name = sys.argv[1]
|
|
||||||
path = sys.argv[3]
|
|
||||||
|
|
||||||
print(f"🚀 Initializing skill: {skill_name}")
|
|
||||||
print(f" Location: {path}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
result = init_skill(skill_name, path)
|
|
||||||
|
|
||||||
if result:
|
|
||||||
sys.exit(0)
|
|
||||||
else:
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Skill Packager - Creates a distributable .skill file of a skill folder
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python utils/package_skill.py <path/to/skill-folder> [output-directory]
|
|
||||||
|
|
||||||
Example:
|
|
||||||
python utils/package_skill.py skills/public/my-skill
|
|
||||||
python utils/package_skill.py skills/public/my-skill ./dist
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import zipfile
|
|
||||||
from pathlib import Path
|
|
||||||
from quick_validate import validate_skill
|
|
||||||
|
|
||||||
|
|
||||||
def package_skill(skill_path, output_dir=None):
|
|
||||||
"""
|
|
||||||
Package a skill folder into a .skill file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
skill_path: Path to the skill folder
|
|
||||||
output_dir: Optional output directory for the .skill file (defaults to current directory)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Path to the created .skill file, or None if error
|
|
||||||
"""
|
|
||||||
skill_path = Path(skill_path).resolve()
|
|
||||||
|
|
||||||
# Validate skill folder exists
|
|
||||||
if not skill_path.exists():
|
|
||||||
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}")
|
|
||||||
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}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Run validation before packaging
|
|
||||||
print("🔍 Validating skill...")
|
|
||||||
valid, message = validate_skill(skill_path)
|
|
||||||
if not valid:
|
|
||||||
print(f"❌ Validation failed: {message}")
|
|
||||||
print(" Please fix the validation errors before packaging.")
|
|
||||||
return None
|
|
||||||
print(f"✅ {message}\n")
|
|
||||||
|
|
||||||
# Determine output location
|
|
||||||
skill_name = skill_path.name
|
|
||||||
if output_dir:
|
|
||||||
output_path = Path(output_dir).resolve()
|
|
||||||
output_path.mkdir(parents=True, exist_ok=True)
|
|
||||||
else:
|
|
||||||
output_path = Path.cwd()
|
|
||||||
|
|
||||||
skill_filename = output_path / f"{skill_name}.skill"
|
|
||||||
|
|
||||||
# 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 file_path.is_file():
|
|
||||||
# Calculate the relative path within the zip
|
|
||||||
arcname = file_path.relative_to(skill_path.parent)
|
|
||||||
zipf.write(file_path, arcname)
|
|
||||||
print(f" Added: {arcname}")
|
|
||||||
|
|
||||||
print(f"\n✅ Successfully packaged skill to: {skill_filename}")
|
|
||||||
return skill_filename
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Error creating .skill file: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
if len(sys.argv) < 2:
|
|
||||||
print("Usage: python utils/package_skill.py <path/to/skill-folder> [output-directory]")
|
|
||||||
print("\nExample:")
|
|
||||||
print(" python utils/package_skill.py skills/public/my-skill")
|
|
||||||
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
|
|
||||||
|
|
||||||
print(f"📦 Packaging skill: {skill_path}")
|
|
||||||
if output_dir:
|
|
||||||
print(f" Output directory: {output_dir}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
result = package_skill(skill_path, output_dir)
|
|
||||||
|
|
||||||
if result:
|
|
||||||
sys.exit(0)
|
|
||||||
else:
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Quick validation script for skills - minimal version
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import yaml
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
def validate_skill(skill_path):
|
|
||||||
"""Basic validation of a skill"""
|
|
||||||
skill_path = Path(skill_path)
|
|
||||||
|
|
||||||
# Check SKILL.md exists
|
|
||||||
skill_md = skill_path / 'SKILL.md'
|
|
||||||
if not skill_md.exists():
|
|
||||||
return False, "SKILL.md not found"
|
|
||||||
|
|
||||||
# Read and validate frontmatter
|
|
||||||
content = skill_md.read_text()
|
|
||||||
if not content.startswith('---'):
|
|
||||||
return False, "No YAML frontmatter found"
|
|
||||||
|
|
||||||
# Extract frontmatter
|
|
||||||
match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
|
|
||||||
if not match:
|
|
||||||
return False, "Invalid frontmatter format"
|
|
||||||
|
|
||||||
frontmatter_text = match.group(1)
|
|
||||||
|
|
||||||
# Parse YAML frontmatter
|
|
||||||
try:
|
|
||||||
frontmatter = yaml.safe_load(frontmatter_text)
|
|
||||||
if not isinstance(frontmatter, dict):
|
|
||||||
return False, "Frontmatter must be a YAML dictionary"
|
|
||||||
except yaml.YAMLError as e:
|
|
||||||
return False, f"Invalid YAML in frontmatter: {e}"
|
|
||||||
|
|
||||||
# Define allowed properties
|
|
||||||
ALLOWED_PROPERTIES = {'name', 'description', 'license', 'allowed-tools', 'metadata'}
|
|
||||||
|
|
||||||
# Check for unexpected properties (excluding nested keys under metadata)
|
|
||||||
unexpected_keys = set(frontmatter.keys()) - ALLOWED_PROPERTIES
|
|
||||||
if unexpected_keys:
|
|
||||||
return False, (
|
|
||||||
f"Unexpected key(s) in SKILL.md frontmatter: {', '.join(sorted(unexpected_keys))}. "
|
|
||||||
f"Allowed properties are: {', '.join(sorted(ALLOWED_PROPERTIES))}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check required fields
|
|
||||||
if 'name' not in frontmatter:
|
|
||||||
return False, "Missing 'name' in frontmatter"
|
|
||||||
if 'description' not in frontmatter:
|
|
||||||
return False, "Missing 'description' in frontmatter"
|
|
||||||
|
|
||||||
# Extract name for validation
|
|
||||||
name = frontmatter.get('name', '')
|
|
||||||
if not isinstance(name, str):
|
|
||||||
return False, f"Name must be a string, got {type(name).__name__}"
|
|
||||||
name = name.strip()
|
|
||||||
if name:
|
|
||||||
# Check naming convention (hyphen-case: lowercase with hyphens)
|
|
||||||
if not re.match(r'^[a-z0-9-]+$', name):
|
|
||||||
return False, f"Name '{name}' should be hyphen-case (lowercase letters, digits, and hyphens only)"
|
|
||||||
if name.startswith('-') or name.endswith('-') or '--' in name:
|
|
||||||
return False, f"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens"
|
|
||||||
# Check name length (max 64 characters per spec)
|
|
||||||
if len(name) > 64:
|
|
||||||
return False, f"Name is too long ({len(name)} characters). Maximum is 64 characters."
|
|
||||||
|
|
||||||
# Extract and validate description
|
|
||||||
description = frontmatter.get('description', '')
|
|
||||||
if not isinstance(description, str):
|
|
||||||
return False, f"Description must be a string, got {type(description).__name__}"
|
|
||||||
description = description.strip()
|
|
||||||
if description:
|
|
||||||
# Check for angle brackets
|
|
||||||
if '<' in description or '>' in description:
|
|
||||||
return False, "Description cannot contain angle brackets (< or >)"
|
|
||||||
# Check description length (max 1024 characters per spec)
|
|
||||||
if len(description) > 1024:
|
|
||||||
return False, f"Description is too long ({len(description)} characters). Maximum is 1024 characters."
|
|
||||||
|
|
||||||
return True, "Skill is valid!"
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
if len(sys.argv) != 2:
|
|
||||||
print("Usage: python quick_validate.py <skill_directory>")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
valid, message = validate_skill(sys.argv[1])
|
|
||||||
print(message)
|
|
||||||
sys.exit(0 if valid else 1)
|
|
||||||
15
build.js
15
build.js
@@ -5,7 +5,7 @@
|
|||||||
* Bundles TypeScript source into a single JavaScript file
|
* Bundles TypeScript source into a single JavaScript file
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFileSync } from "node:fs";
|
import { cpSync, existsSync, readFileSync, rmSync } from "node:fs";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
@@ -54,6 +54,19 @@ await Bun.write(outputPath, withShebang);
|
|||||||
// Make executable
|
// Make executable
|
||||||
await Bun.$`chmod +x letta.js`;
|
await Bun.$`chmod +x letta.js`;
|
||||||
|
|
||||||
|
// Copy bundled skills to skills/ directory for shipping
|
||||||
|
const bundledSkillsSrc = join(__dirname, "src/skills/builtin");
|
||||||
|
const bundledSkillsDst = join(__dirname, "skills");
|
||||||
|
|
||||||
|
if (existsSync(bundledSkillsSrc)) {
|
||||||
|
// Clean and copy
|
||||||
|
if (existsSync(bundledSkillsDst)) {
|
||||||
|
rmSync(bundledSkillsDst, { recursive: true });
|
||||||
|
}
|
||||||
|
cpSync(bundledSkillsSrc, bundledSkillsDst, { recursive: true });
|
||||||
|
console.log("📂 Copied bundled skills to skills/");
|
||||||
|
}
|
||||||
|
|
||||||
console.log("✅ Build complete!");
|
console.log("✅ Build complete!");
|
||||||
console.log(` Output: letta.js`);
|
console.log(` Output: letta.js`);
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"README.md",
|
"README.md",
|
||||||
"letta.js",
|
"letta.js",
|
||||||
"scripts",
|
"scripts",
|
||||||
|
"skills",
|
||||||
"vendor"
|
"vendor"
|
||||||
],
|
],
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -9,17 +9,27 @@
|
|||||||
|
|
||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { readdir, readFile } from "node:fs/promises";
|
import { readdir, readFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
// Import bundled skills (embedded at build time)
|
import { fileURLToPath } from "node:url";
|
||||||
import memoryInitSkillMd from "../skills/builtin/memory-init/SKILL.md";
|
|
||||||
import { parseFrontmatter } from "../utils/frontmatter";
|
import { parseFrontmatter } from "../utils/frontmatter";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bundled skill sources - embedded at build time
|
* Get the bundled skills directory path
|
||||||
|
* This is where skills ship with the package (skills/ directory next to letta.js)
|
||||||
*/
|
*/
|
||||||
const BUNDLED_SKILL_SOURCES: Array<{ id: string; content: string }> = [
|
function getBundledSkillsPath(): string {
|
||||||
{ id: "memory-init", content: memoryInitSkillMd },
|
// In dev mode (running from src/), look in src/skills/builtin/
|
||||||
];
|
// In production (running from letta.js), look in skills/ next to letta.js
|
||||||
|
const thisDir = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
// Check if we're in dev mode (thisDir contains 'src/agent')
|
||||||
|
if (thisDir.includes("src/agent") || thisDir.includes("src\\agent")) {
|
||||||
|
return join(thisDir, "../skills/builtin");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Production mode - skills/ is next to the bundled letta.js
|
||||||
|
return join(thisDir, "skills");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Source of a skill (for display and override resolution)
|
* Source of a skill (for display and override resolution)
|
||||||
@@ -90,65 +100,13 @@ const SKILLS_BLOCK_CHAR_LIMIT = 20000;
|
|||||||
/**
|
/**
|
||||||
* Parse a bundled skill from its embedded content
|
* Parse a bundled skill from its embedded content
|
||||||
*/
|
*/
|
||||||
function parseBundledSkill(id: string, content: string): Skill {
|
|
||||||
const { frontmatter, body } = parseFrontmatter(content);
|
|
||||||
|
|
||||||
const name =
|
|
||||||
(typeof frontmatter.name === "string" ? frontmatter.name : null) ||
|
|
||||||
id
|
|
||||||
.split("/")
|
|
||||||
.pop()
|
|
||||||
?.replace(/-/g, " ")
|
|
||||||
.replace(/\b\w/g, (l) => l.toUpperCase()) ||
|
|
||||||
id;
|
|
||||||
|
|
||||||
let description =
|
|
||||||
typeof frontmatter.description === "string"
|
|
||||||
? frontmatter.description
|
|
||||||
: null;
|
|
||||||
if (!description) {
|
|
||||||
const firstParagraph = body.trim().split("\n\n")[0];
|
|
||||||
description = firstParagraph || "No description available";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip surrounding quotes
|
|
||||||
description = description.trim();
|
|
||||||
if (
|
|
||||||
(description.startsWith('"') && description.endsWith('"')) ||
|
|
||||||
(description.startsWith("'") && description.endsWith("'"))
|
|
||||||
) {
|
|
||||||
description = description.slice(1, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let tags: string[] | undefined;
|
|
||||||
if (Array.isArray(frontmatter.tags)) {
|
|
||||||
tags = frontmatter.tags;
|
|
||||||
} else if (typeof frontmatter.tags === "string") {
|
|
||||||
tags = [frontmatter.tags];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
category:
|
|
||||||
typeof frontmatter.category === "string"
|
|
||||||
? frontmatter.category
|
|
||||||
: undefined,
|
|
||||||
tags,
|
|
||||||
path: "", // Bundled skills don't have a file path
|
|
||||||
source: "bundled",
|
|
||||||
content, // Store the full content for bundled skills
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get bundled skills (embedded at build time)
|
* Get bundled skills by discovering from the bundled skills directory
|
||||||
*/
|
*/
|
||||||
export function getBundledSkills(): Skill[] {
|
export async function getBundledSkills(): Promise<Skill[]> {
|
||||||
return BUNDLED_SKILL_SOURCES.map(({ id, content }) =>
|
const bundledPath = getBundledSkillsPath();
|
||||||
parseBundledSkill(id, content),
|
const result = await discoverSkillsFromDir(bundledPath, "bundled");
|
||||||
);
|
return result.skills;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -202,7 +160,8 @@ export async function discoverSkills(
|
|||||||
const skillsById = new Map<string, Skill>();
|
const skillsById = new Map<string, Skill>();
|
||||||
|
|
||||||
// 1. Start with bundled skills (lowest priority)
|
// 1. Start with bundled skills (lowest priority)
|
||||||
for (const skill of getBundledSkills()) {
|
const bundledSkills = await getBundledSkills();
|
||||||
|
for (const skill of bundledSkills) {
|
||||||
skillsById.set(skill.id, skill);
|
skillsById.set(skill.id, skill);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: skill-creator
|
name: skill-creator
|
||||||
description: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Letta Code's capabilities with specialized knowledge, workflows, or tool integrations.
|
description: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Letta Code's capabilities with specialized knowledge, workflows, or tool integrations.
|
||||||
license: Complete terms in LICENSE.txt
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Skill Creator
|
# Skill Creator
|
||||||
@@ -56,7 +55,7 @@ skill-name/
|
|||||||
│ │ └── description: (required)
|
│ │ └── description: (required)
|
||||||
│ └── Markdown instructions (required)
|
│ └── Markdown instructions (required)
|
||||||
└── Bundled Resources (optional)
|
└── Bundled Resources (optional)
|
||||||
├── scripts/ - Executable code (Python/Bash/etc.)
|
├── scripts/ - Executable code (TypeScript/Python/Bash/etc.)
|
||||||
├── references/ - Documentation intended to be loaded into context as needed
|
├── references/ - Documentation intended to be loaded into context as needed
|
||||||
└── assets/ - Files used in output (templates, icons, fonts, etc.)
|
└── assets/ - Files used in output (templates, icons, fonts, etc.)
|
||||||
```
|
```
|
||||||
@@ -72,10 +71,10 @@ Every SKILL.md in Letta Code consists of:
|
|||||||
|
|
||||||
##### Scripts (`scripts/`)
|
##### Scripts (`scripts/`)
|
||||||
|
|
||||||
Executable code (Python/Bash/etc.) for tasks that require deterministic reliability or are repeatedly rewritten.
|
Executable code (TypeScript/Python/Bash/etc.) for tasks that require deterministic reliability or are repeatedly rewritten.
|
||||||
|
|
||||||
- **When to include**: When the same code is being rewritten repeatedly or deterministic reliability is needed
|
- **When to include**: When the same code is being rewritten repeatedly or deterministic reliability is needed
|
||||||
- **Example**: `scripts/rotate_pdf.py` for PDF rotation tasks
|
- **Example**: `scripts/rotate-pdf.ts` for PDF rotation tasks
|
||||||
- **Benefits**: Token efficient, deterministic, may be executed without loading into context
|
- **Benefits**: Token efficient, deterministic, may be executed without loading into context
|
||||||
- **Note**: Scripts may still need to be read by the Letta Code agent for patching or environment-specific adjustments
|
- **Note**: Scripts may still need to be read by the Letta Code agent for patching or environment-specific adjustments
|
||||||
|
|
||||||
@@ -205,9 +204,9 @@ Skill creation involves these steps:
|
|||||||
|
|
||||||
1. Understand the skill with concrete examples
|
1. Understand the skill with concrete examples
|
||||||
2. Plan reusable skill contents (scripts, references, assets)
|
2. Plan reusable skill contents (scripts, references, assets)
|
||||||
3. Initialize the skill (run init_skill.py)
|
3. Initialize the skill (run init-skill.ts)
|
||||||
4. Edit the skill (implement resources and write SKILL.md)
|
4. Edit the skill (implement resources and write SKILL.md)
|
||||||
5. Package the skill (run package_skill.py)
|
5. Package the skill (run package-skill.ts)
|
||||||
6. Iterate based on real usage
|
6. Iterate based on real usage
|
||||||
|
|
||||||
Follow these steps in order, skipping only if there is a clear reason why they are not applicable.
|
Follow these steps in order, skipping only if there is a clear reason why they are not applicable.
|
||||||
@@ -239,7 +238,7 @@ To turn concrete examples into an effective skill, analyze each example by:
|
|||||||
Example: When building a `pdf-editor` skill to handle queries like "Help me rotate this PDF," the analysis shows:
|
Example: When building a `pdf-editor` skill to handle queries like "Help me rotate this PDF," the analysis shows:
|
||||||
|
|
||||||
1. Rotating a PDF requires re-writing the same code each time
|
1. Rotating a PDF requires re-writing the same code each time
|
||||||
2. A `scripts/rotate_pdf.py` script would be helpful to store in the skill
|
2. A `scripts/rotate-pdf.ts` script would be helpful to store in the skill
|
||||||
|
|
||||||
Example: When designing a `frontend-webapp-builder` skill for queries like "Build me a todo app" or "Build me a dashboard to track my steps," the analysis shows:
|
Example: When designing a `frontend-webapp-builder` skill for queries like "Build me a todo app" or "Build me a dashboard to track my steps," the analysis shows:
|
||||||
|
|
||||||
@@ -259,12 +258,12 @@ At this point, it is time to actually create the skill.
|
|||||||
|
|
||||||
Skip this step only if the skill being developed already exists, and iteration or packaging is needed. In this case, continue to the next step.
|
Skip this step only if the skill being developed already exists, and iteration or packaging is needed. In this case, continue to the next step.
|
||||||
|
|
||||||
When creating a new skill from scratch, always run the `init_skill.py` script. The script conveniently generates a new template skill directory that automatically includes everything a skill requires, making the skill creation process much more efficient and reliable.
|
When creating a new skill from scratch, always run the `init-skill.ts` script. The script conveniently generates a new template skill directory that automatically includes everything a skill requires, making the skill creation process much more efficient and reliable.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scripts/init_skill.py <skill-name> --path <output-directory>
|
npx ts-node scripts/init-skill.ts <skill-name> --path <output-directory>
|
||||||
```
|
```
|
||||||
|
|
||||||
The script:
|
The script:
|
||||||
@@ -322,13 +321,13 @@ Write instructions for using the skill and its bundled resources.
|
|||||||
Once development of the skill is complete, it must be packaged into a distributable .skill file that gets shared with the user. The packaging process automatically validates the skill first to ensure it meets all requirements:
|
Once development of the skill is complete, it must be packaged into a distributable .skill file that gets shared with the user. The packaging process automatically validates the skill first to ensure it meets all requirements:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scripts/package_skill.py <path/to/skill-folder>
|
npx ts-node scripts/package-skill.ts <path/to/skill-folder>
|
||||||
```
|
```
|
||||||
|
|
||||||
Optional output directory specification:
|
Optional output directory specification:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scripts/package_skill.py <path/to/skill-folder> ./dist
|
npx ts-node scripts/package-skill.ts <path/to/skill-folder> ./dist
|
||||||
```
|
```
|
||||||
|
|
||||||
The packaging script will:
|
The packaging script will:
|
||||||
@@ -7,11 +7,11 @@ For complex tasks, break operations into clear, sequential steps. It is often he
|
|||||||
```markdown
|
```markdown
|
||||||
Filling a PDF form involves these steps:
|
Filling a PDF form involves these steps:
|
||||||
|
|
||||||
1. Analyze the form (run analyze_form.py)
|
1. Analyze the form (run analyze-form.ts)
|
||||||
2. Create field mapping (edit fields.json)
|
2. Create field mapping (edit fields.json)
|
||||||
3. Validate mapping (run validate_fields.py)
|
3. Validate mapping (run validate-fields.ts)
|
||||||
4. Fill the form (run fill_form.py)
|
4. Fill the form (run fill-form.ts)
|
||||||
5. Verify output (run verify_output.py)
|
5. Verify output (run verify-output.ts)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Conditional Workflows
|
## Conditional Workflows
|
||||||
@@ -25,4 +25,4 @@ For tasks with branching logic, guide the Letta Code agent through decision poin
|
|||||||
|
|
||||||
2. Creation workflow: [steps]
|
2. Creation workflow: [steps]
|
||||||
3. Editing workflow: [steps]
|
3. Editing workflow: [steps]
|
||||||
```
|
```
|
||||||
279
src/skills/builtin/skill-creator/scripts/init-skill.ts
Normal file
279
src/skills/builtin/skill-creator/scripts/init-skill.ts
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
#!/usr/bin/env npx ts-node
|
||||||
|
/**
|
||||||
|
* Skill Initializer - Creates a new skill from template
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* npx ts-node init-skill.ts <skill-name> --path <path>
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* npx ts-node init-skill.ts my-new-skill --path .skills
|
||||||
|
* npx ts-node init-skill.ts my-api-helper --path ~/.letta/skills
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||||
|
import { join, resolve } from "node:path";
|
||||||
|
|
||||||
|
const 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.]"
|
||||||
|
---
|
||||||
|
|
||||||
|
# {skill_title}
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
[TODO: 1-2 sentences explaining what this skill enables]
|
||||||
|
|
||||||
|
## Structuring This Skill
|
||||||
|
|
||||||
|
[TODO: Choose the structure that best fits this skill's purpose. Common patterns:
|
||||||
|
|
||||||
|
**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...
|
||||||
|
|
||||||
|
**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...
|
||||||
|
|
||||||
|
**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...
|
||||||
|
|
||||||
|
**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...
|
||||||
|
|
||||||
|
Patterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations).
|
||||||
|
|
||||||
|
Delete this entire "Structuring This Skill" section when done - it's just guidance.]
|
||||||
|
|
||||||
|
## [TODO: Replace with the first main section based on chosen structure]
|
||||||
|
|
||||||
|
[TODO: Add content here. See examples in existing skills:
|
||||||
|
- Code samples for technical skills
|
||||||
|
- Decision trees for complex workflows
|
||||||
|
- Concrete examples with realistic user requests
|
||||||
|
- References to scripts/templates/references as needed]
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
This skill includes example resource directories that demonstrate how to organize different types of bundled resources:
|
||||||
|
|
||||||
|
### scripts/
|
||||||
|
Executable code (TypeScript/Python/Bash/etc.) that can be run directly to perform specific operations.
|
||||||
|
|
||||||
|
**Appropriate for:** Scripts that perform automation, data processing, or specific operations.
|
||||||
|
|
||||||
|
**Note:** Scripts may be executed without loading into context, but can still be read by the Letta Code agent for patching or environment adjustments.
|
||||||
|
|
||||||
|
### references/
|
||||||
|
Documentation and reference material intended to be loaded into context to inform the Letta Code agent's process and thinking.
|
||||||
|
|
||||||
|
**Appropriate for:** In-depth documentation, API references, database schemas, comprehensive guides, or any detailed information that the Letta Code agent should reference while working.
|
||||||
|
|
||||||
|
### assets/
|
||||||
|
Files not intended to be loaded into context, but rather used within the output the Letta Code agent produces.
|
||||||
|
|
||||||
|
**Appropriate for:** Templates, boilerplate code, document templates, images, icons, fonts, or any files meant to be copied or used in the final output.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Any unneeded directories can be deleted.** Not every skill requires all three types of resources.
|
||||||
|
`;
|
||||||
|
|
||||||
|
const EXAMPLE_SCRIPT = `#!/usr/bin/env npx ts-node
|
||||||
|
/**
|
||||||
|
* Example helper script for {skill_name}
|
||||||
|
*
|
||||||
|
* This is a placeholder script that can be executed directly.
|
||||||
|
* Replace with actual implementation or delete if not needed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
console.log("This is an example script for {skill_name}");
|
||||||
|
// TODO: Add actual script logic here
|
||||||
|
// This could be data processing, file conversion, API calls, etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
`;
|
||||||
|
|
||||||
|
const EXAMPLE_REFERENCE = `# Reference Documentation for {skill_title}
|
||||||
|
|
||||||
|
This is a placeholder for detailed reference documentation.
|
||||||
|
Replace with actual reference content or delete if not needed.
|
||||||
|
|
||||||
|
## When Reference Docs Are Useful
|
||||||
|
|
||||||
|
Reference docs are ideal for:
|
||||||
|
- Comprehensive API documentation
|
||||||
|
- Detailed workflow guides
|
||||||
|
- Complex multi-step processes
|
||||||
|
- Information too lengthy for main SKILL.md
|
||||||
|
- Content that's only needed for specific use cases
|
||||||
|
|
||||||
|
## Structure Suggestions
|
||||||
|
|
||||||
|
### API Reference Example
|
||||||
|
- Overview
|
||||||
|
- Authentication
|
||||||
|
- Endpoints with examples
|
||||||
|
- Error codes
|
||||||
|
- Rate limits
|
||||||
|
|
||||||
|
### Workflow Guide Example
|
||||||
|
- Prerequisites
|
||||||
|
- Step-by-step instructions
|
||||||
|
- Common patterns
|
||||||
|
- Troubleshooting
|
||||||
|
- Best practices
|
||||||
|
`;
|
||||||
|
|
||||||
|
const EXAMPLE_ASSET = `# Example Asset File
|
||||||
|
|
||||||
|
This placeholder represents where asset files would be stored.
|
||||||
|
Replace with actual asset files (templates, images, fonts, etc.) or delete if not needed.
|
||||||
|
|
||||||
|
Asset files are NOT intended to be loaded into context, but rather used within
|
||||||
|
the output the Letta Code agent produces.
|
||||||
|
|
||||||
|
## Common Asset Types
|
||||||
|
|
||||||
|
- Templates: .pptx, .docx, boilerplate directories
|
||||||
|
- Images: .png, .jpg, .svg, .gif
|
||||||
|
- Fonts: .ttf, .otf, .woff, .woff2
|
||||||
|
- Boilerplate code: Project directories, starter files
|
||||||
|
- Icons: .ico, .svg
|
||||||
|
- Data files: .csv, .json, .xml, .yaml
|
||||||
|
|
||||||
|
Note: This is a text placeholder. Actual assets can be any file type.
|
||||||
|
`;
|
||||||
|
|
||||||
|
function titleCaseSkillName(skillName: string): string {
|
||||||
|
return skillName
|
||||||
|
.split("-")
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSkill(skillName: string, path: string): string | null {
|
||||||
|
const skillDir = resolve(path, skillName);
|
||||||
|
|
||||||
|
// Check if directory already exists
|
||||||
|
if (existsSync(skillDir)) {
|
||||||
|
console.error(`Error: Skill directory already exists: ${skillDir}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create skill directory
|
||||||
|
try {
|
||||||
|
mkdirSync(skillDir, { recursive: true });
|
||||||
|
console.log(`Created skill directory: ${skillDir}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`Error creating directory: ${e instanceof Error ? e.message : String(e)}`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const skillTitle = titleCaseSkillName(skillName);
|
||||||
|
|
||||||
|
// Create SKILL.md from template
|
||||||
|
const skillContent = SKILL_TEMPLATE.replace(
|
||||||
|
/{skill_name}/g,
|
||||||
|
skillName,
|
||||||
|
).replace(/{skill_title}/g, skillTitle);
|
||||||
|
|
||||||
|
try {
|
||||||
|
writeFileSync(join(skillDir, "SKILL.md"), skillContent);
|
||||||
|
console.log("Created SKILL.md");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`Error creating SKILL.md: ${e instanceof Error ? e.message : String(e)}`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create resource directories with example files
|
||||||
|
try {
|
||||||
|
// Create scripts/ directory with example script
|
||||||
|
const scriptsDir = join(skillDir, "scripts");
|
||||||
|
mkdirSync(scriptsDir, { recursive: true });
|
||||||
|
const exampleScriptPath = join(scriptsDir, "example.ts");
|
||||||
|
writeFileSync(
|
||||||
|
exampleScriptPath,
|
||||||
|
EXAMPLE_SCRIPT.replace(/{skill_name}/g, skillName),
|
||||||
|
);
|
||||||
|
chmodSync(exampleScriptPath, 0o755);
|
||||||
|
console.log("Created scripts/example.ts");
|
||||||
|
|
||||||
|
// Create references/ directory with example reference doc
|
||||||
|
const referencesDir = join(skillDir, "references");
|
||||||
|
mkdirSync(referencesDir, { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
join(referencesDir, "api-reference.md"),
|
||||||
|
EXAMPLE_REFERENCE.replace(/{skill_title}/g, skillTitle),
|
||||||
|
);
|
||||||
|
console.log("Created references/api-reference.md");
|
||||||
|
|
||||||
|
// Create assets/ directory with example asset placeholder
|
||||||
|
const assetsDir = join(skillDir, "assets");
|
||||||
|
mkdirSync(assetsDir, { recursive: true });
|
||||||
|
writeFileSync(join(assetsDir, "example-asset.txt"), EXAMPLE_ASSET);
|
||||||
|
console.log("Created assets/example-asset.txt");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`Error creating resource directories: ${e instanceof Error ? e.message : String(e)}`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print next steps
|
||||||
|
console.log(`\nSkill '${skillName}' initialized successfully at ${skillDir}`);
|
||||||
|
console.log("\nNext steps:");
|
||||||
|
console.log(
|
||||||
|
"1. Edit SKILL.md to complete the TODO items and update the description",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"2. Customize or delete the example files in scripts/, references/, and assets/",
|
||||||
|
);
|
||||||
|
console.log("3. Run the validator when ready to check the skill structure");
|
||||||
|
|
||||||
|
return skillDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI entry point
|
||||||
|
if (require.main === module) {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length < 3 || args[1] !== "--path") {
|
||||||
|
console.log("Usage: npx ts-node init-skill.ts <skill-name> --path <path>");
|
||||||
|
console.log("\nSkill name requirements:");
|
||||||
|
console.log(" - Hyphen-case identifier (e.g., 'data-analyzer')");
|
||||||
|
console.log(" - Lowercase letters, digits, and hyphens only");
|
||||||
|
console.log(" - Max 64 characters");
|
||||||
|
console.log(" - Must match directory name exactly");
|
||||||
|
console.log("\nExamples:");
|
||||||
|
console.log(" npx ts-node init-skill.ts my-new-skill --path .skills");
|
||||||
|
console.log(
|
||||||
|
" npx ts-node init-skill.ts my-api-helper --path ~/.letta/skills",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const skillName = args[0] as string;
|
||||||
|
const path = args[2] as string;
|
||||||
|
|
||||||
|
console.log(`Initializing skill: ${skillName}`);
|
||||||
|
console.log(`Location: ${path}\n`);
|
||||||
|
|
||||||
|
const result = initSkill(skillName, path);
|
||||||
|
process.exit(result ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { initSkill, titleCaseSkillName };
|
||||||
268
src/skills/builtin/skill-creator/scripts/package-skill.ts
Normal file
268
src/skills/builtin/skill-creator/scripts/package-skill.ts
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
#!/usr/bin/env npx ts-node
|
||||||
|
/**
|
||||||
|
* Skill Packager - Creates a distributable .skill file of a skill folder
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* npx ts-node package-skill.ts <path/to/skill-folder> [output-directory]
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* npx ts-node package-skill.ts .skills/my-skill
|
||||||
|
* npx ts-node package-skill.ts .skills/my-skill ./dist
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
existsSync,
|
||||||
|
mkdirSync,
|
||||||
|
readdirSync,
|
||||||
|
readFileSync,
|
||||||
|
statSync,
|
||||||
|
writeFileSync,
|
||||||
|
} from "node:fs";
|
||||||
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
||||||
|
// Simple zip implementation using Node.js built-in zlib
|
||||||
|
// For a proper zip file, we'll create the structure manually
|
||||||
|
import { deflateSync } from "node:zlib";
|
||||||
|
import { validateSkill } from "./validate-skill";
|
||||||
|
|
||||||
|
interface ZipEntry {
|
||||||
|
path: string;
|
||||||
|
data: Buffer;
|
||||||
|
isDirectory: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createZip(entries: ZipEntry[]): Buffer {
|
||||||
|
// Use archiver-like approach with raw buffer manipulation
|
||||||
|
// For simplicity, we'll use a basic ZIP format
|
||||||
|
|
||||||
|
const localHeaders: Buffer[] = [];
|
||||||
|
const centralHeaders: Buffer[] = [];
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const pathBuffer = Buffer.from(entry.path, "utf8");
|
||||||
|
const compressedData = entry.isDirectory
|
||||||
|
? Buffer.alloc(0)
|
||||||
|
: deflateSync(entry.data);
|
||||||
|
const uncompressedSize = entry.isDirectory ? 0 : entry.data.length;
|
||||||
|
const compressedSize = compressedData.length;
|
||||||
|
|
||||||
|
// CRC32 calculation
|
||||||
|
const crc = entry.isDirectory ? 0 : crc32(entry.data);
|
||||||
|
|
||||||
|
// Local file header
|
||||||
|
const localHeader = Buffer.alloc(30 + pathBuffer.length);
|
||||||
|
localHeader.writeUInt32LE(0x04034b50, 0); // Local file header signature
|
||||||
|
localHeader.writeUInt16LE(20, 4); // Version needed to extract
|
||||||
|
localHeader.writeUInt16LE(0, 6); // General purpose bit flag
|
||||||
|
localHeader.writeUInt16LE(entry.isDirectory ? 0 : 8, 8); // Compression method (8 = deflate)
|
||||||
|
localHeader.writeUInt16LE(0, 10); // File last modification time
|
||||||
|
localHeader.writeUInt16LE(0, 12); // File last modification date
|
||||||
|
localHeader.writeUInt32LE(crc, 14); // CRC-32
|
||||||
|
localHeader.writeUInt32LE(compressedSize, 18); // Compressed size
|
||||||
|
localHeader.writeUInt32LE(uncompressedSize, 22); // Uncompressed size
|
||||||
|
localHeader.writeUInt16LE(pathBuffer.length, 26); // File name length
|
||||||
|
localHeader.writeUInt16LE(0, 28); // Extra field length
|
||||||
|
pathBuffer.copy(localHeader, 30);
|
||||||
|
|
||||||
|
localHeaders.push(localHeader);
|
||||||
|
localHeaders.push(compressedData);
|
||||||
|
|
||||||
|
// Central directory header
|
||||||
|
const centralHeader = Buffer.alloc(46 + pathBuffer.length);
|
||||||
|
centralHeader.writeUInt32LE(0x02014b50, 0); // Central directory signature
|
||||||
|
centralHeader.writeUInt16LE(20, 4); // Version made by
|
||||||
|
centralHeader.writeUInt16LE(20, 6); // Version needed to extract
|
||||||
|
centralHeader.writeUInt16LE(0, 8); // General purpose bit flag
|
||||||
|
centralHeader.writeUInt16LE(entry.isDirectory ? 0 : 8, 10); // Compression method
|
||||||
|
centralHeader.writeUInt16LE(0, 12); // File last modification time
|
||||||
|
centralHeader.writeUInt16LE(0, 14); // File last modification date
|
||||||
|
centralHeader.writeUInt32LE(crc, 16); // CRC-32
|
||||||
|
centralHeader.writeUInt32LE(compressedSize, 20); // Compressed size
|
||||||
|
centralHeader.writeUInt32LE(uncompressedSize, 24); // Uncompressed size
|
||||||
|
centralHeader.writeUInt16LE(pathBuffer.length, 28); // File name length
|
||||||
|
centralHeader.writeUInt16LE(0, 30); // Extra field length
|
||||||
|
centralHeader.writeUInt16LE(0, 32); // File comment length
|
||||||
|
centralHeader.writeUInt16LE(0, 34); // Disk number start
|
||||||
|
centralHeader.writeUInt16LE(0, 36); // Internal file attributes
|
||||||
|
centralHeader.writeUInt32LE(entry.isDirectory ? 0x10 : 0, 38); // External file attributes
|
||||||
|
centralHeader.writeUInt32LE(offset, 42); // Relative offset of local header
|
||||||
|
pathBuffer.copy(centralHeader, 46);
|
||||||
|
|
||||||
|
centralHeaders.push(centralHeader);
|
||||||
|
|
||||||
|
offset += localHeader.length + compressedData.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const centralDirOffset = offset;
|
||||||
|
const centralDirSize = centralHeaders.reduce((sum, h) => sum + h.length, 0);
|
||||||
|
|
||||||
|
// End of central directory record
|
||||||
|
const endRecord = Buffer.alloc(22);
|
||||||
|
endRecord.writeUInt32LE(0x06054b50, 0); // End of central directory signature
|
||||||
|
endRecord.writeUInt16LE(0, 4); // Number of this disk
|
||||||
|
endRecord.writeUInt16LE(0, 6); // Disk where central directory starts
|
||||||
|
endRecord.writeUInt16LE(entries.length, 8); // Number of central directory records on this disk
|
||||||
|
endRecord.writeUInt16LE(entries.length, 10); // Total number of central directory records
|
||||||
|
endRecord.writeUInt32LE(centralDirSize, 12); // Size of central directory
|
||||||
|
endRecord.writeUInt32LE(centralDirOffset, 16); // Offset of start of central directory
|
||||||
|
endRecord.writeUInt16LE(0, 20); // Comment length
|
||||||
|
|
||||||
|
return Buffer.concat([...localHeaders, ...centralHeaders, endRecord]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRC32 implementation
|
||||||
|
function crc32(data: Buffer): number {
|
||||||
|
let crc = 0xffffffff;
|
||||||
|
const table = getCrc32Table();
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
// biome-ignore lint/style/noNonNullAssertion: array access within bounds
|
||||||
|
crc = (crc >>> 8) ^ (table[(crc ^ data[i]!) & 0xff] as number);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (crc ^ 0xffffffff) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let crc32Table: number[] | null = null;
|
||||||
|
function getCrc32Table(): number[] {
|
||||||
|
if (crc32Table) return crc32Table;
|
||||||
|
|
||||||
|
crc32Table = [];
|
||||||
|
for (let i = 0; i < 256; i++) {
|
||||||
|
let c = i;
|
||||||
|
for (let j = 0; j < 8; j++) {
|
||||||
|
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||||
|
}
|
||||||
|
crc32Table[i] = c;
|
||||||
|
}
|
||||||
|
return crc32Table;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllFiles(dir: string, baseDir: string): ZipEntry[] {
|
||||||
|
const entries: ZipEntry[] = [];
|
||||||
|
const items = readdirSync(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const fullPath = join(dir, item.name);
|
||||||
|
const relativePath = relative(baseDir, fullPath);
|
||||||
|
|
||||||
|
if (item.isDirectory()) {
|
||||||
|
entries.push({
|
||||||
|
path: `${relativePath}/`,
|
||||||
|
data: Buffer.alloc(0),
|
||||||
|
isDirectory: true,
|
||||||
|
});
|
||||||
|
entries.push(...getAllFiles(fullPath, baseDir));
|
||||||
|
} else {
|
||||||
|
entries.push({
|
||||||
|
path: relativePath,
|
||||||
|
data: readFileSync(fullPath),
|
||||||
|
isDirectory: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
function packageSkill(skillPath: string, outputDir?: string): string | null {
|
||||||
|
const resolvedPath = resolve(skillPath);
|
||||||
|
|
||||||
|
// Validate skill folder exists
|
||||||
|
if (!existsSync(resolvedPath)) {
|
||||||
|
console.error(`Error: Skill folder not found: ${resolvedPath}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!statSync(resolvedPath).isDirectory()) {
|
||||||
|
console.error(`Error: Path is not a directory: ${resolvedPath}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate SKILL.md exists
|
||||||
|
const skillMdPath = join(resolvedPath, "SKILL.md");
|
||||||
|
if (!existsSync(skillMdPath)) {
|
||||||
|
console.error(`Error: SKILL.md not found in ${resolvedPath}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run validation before packaging
|
||||||
|
console.log("Validating skill...");
|
||||||
|
const { valid, message } = validateSkill(resolvedPath);
|
||||||
|
if (!valid) {
|
||||||
|
console.error(`Validation failed: ${message}`);
|
||||||
|
console.error("Please fix the validation errors before packaging.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
console.log(`${message}\n`);
|
||||||
|
|
||||||
|
// Determine output location
|
||||||
|
const skillName = basename(resolvedPath);
|
||||||
|
const outputPath = outputDir ? resolve(outputDir) : process.cwd();
|
||||||
|
|
||||||
|
if (outputDir && !existsSync(outputPath)) {
|
||||||
|
mkdirSync(outputPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const skillFilename = join(outputPath, `${skillName}.skill`);
|
||||||
|
|
||||||
|
// Create the .skill file (zip format)
|
||||||
|
try {
|
||||||
|
// Get all files, using parent directory as base so skill folder is included
|
||||||
|
const parentDir = dirname(resolvedPath);
|
||||||
|
const entries = getAllFiles(resolvedPath, parentDir);
|
||||||
|
|
||||||
|
// Add the skill directory itself
|
||||||
|
entries.unshift({
|
||||||
|
path: `${skillName}/`,
|
||||||
|
data: Buffer.alloc(0),
|
||||||
|
isDirectory: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const zipBuffer = createZip(entries);
|
||||||
|
writeFileSync(skillFilename, zipBuffer);
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry.isDirectory) {
|
||||||
|
console.log(` Added: ${entry.path}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nSuccessfully packaged skill to: ${skillFilename}`);
|
||||||
|
return skillFilename;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`Error creating .skill file: ${e instanceof Error ? e.message : String(e)}`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI entry point
|
||||||
|
if (require.main === module) {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length < 1) {
|
||||||
|
console.log(
|
||||||
|
"Usage: npx ts-node package-skill.ts <path/to/skill-folder> [output-directory]",
|
||||||
|
);
|
||||||
|
console.log("\nExample:");
|
||||||
|
console.log(" npx ts-node package-skill.ts .skills/my-skill");
|
||||||
|
console.log(" npx ts-node package-skill.ts .skills/my-skill ./dist");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const skillPath = args[0] as string;
|
||||||
|
const outputDir = args[1];
|
||||||
|
|
||||||
|
console.log(`Packaging skill: ${skillPath}`);
|
||||||
|
if (outputDir) {
|
||||||
|
console.log(`Output directory: ${outputDir}`);
|
||||||
|
}
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
const result = packageSkill(skillPath, outputDir);
|
||||||
|
process.exit(result ? 0 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { packageSkill };
|
||||||
161
src/skills/builtin/skill-creator/scripts/validate-skill.ts
Normal file
161
src/skills/builtin/skill-creator/scripts/validate-skill.ts
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
#!/usr/bin/env npx ts-node
|
||||||
|
/**
|
||||||
|
* Skill Validator - Validates skill structure and frontmatter
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* npx ts-node validate-skill.ts <skill-directory>
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* npx ts-node validate-skill.ts .skills/my-skill
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { parse as parseYaml } from "yaml";
|
||||||
|
|
||||||
|
interface ValidationResult {
|
||||||
|
valid: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALLOWED_PROPERTIES = new Set([
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"license",
|
||||||
|
"allowed-tools",
|
||||||
|
"metadata",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function validateSkill(skillPath: string): ValidationResult {
|
||||||
|
// Check SKILL.md exists
|
||||||
|
const skillMdPath = join(skillPath, "SKILL.md");
|
||||||
|
if (!existsSync(skillMdPath)) {
|
||||||
|
return { valid: false, message: "SKILL.md not found" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read content
|
||||||
|
const content = readFileSync(skillMdPath, "utf-8");
|
||||||
|
|
||||||
|
// Check for frontmatter
|
||||||
|
if (!content.startsWith("---")) {
|
||||||
|
return { valid: false, message: "No YAML frontmatter found" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract frontmatter
|
||||||
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
||||||
|
if (!match) {
|
||||||
|
return { valid: false, message: "Invalid frontmatter format" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const frontmatterText = match[1] as string;
|
||||||
|
|
||||||
|
// Parse YAML frontmatter
|
||||||
|
let frontmatter: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
frontmatter = parseYaml(frontmatterText);
|
||||||
|
if (typeof frontmatter !== "object" || frontmatter === null) {
|
||||||
|
return { valid: false, message: "Frontmatter must be a YAML dictionary" };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `Invalid YAML in frontmatter: ${e instanceof Error ? e.message : String(e)}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for unexpected properties
|
||||||
|
const unexpectedKeys = Object.keys(frontmatter).filter(
|
||||||
|
(key) => !ALLOWED_PROPERTIES.has(key),
|
||||||
|
);
|
||||||
|
if (unexpectedKeys.length > 0) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `Unexpected key(s) in SKILL.md frontmatter: ${unexpectedKeys.sort().join(", ")}. Allowed properties are: ${[...ALLOWED_PROPERTIES].sort().join(", ")}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check required fields
|
||||||
|
if (!("name" in frontmatter)) {
|
||||||
|
return { valid: false, message: "Missing 'name' in frontmatter" };
|
||||||
|
}
|
||||||
|
if (!("description" in frontmatter)) {
|
||||||
|
return { valid: false, message: "Missing 'description' in frontmatter" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate name
|
||||||
|
const name = frontmatter.name;
|
||||||
|
if (typeof name !== "string") {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `Name must be a string, got ${typeof name}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const trimmedName = name.trim();
|
||||||
|
if (trimmedName) {
|
||||||
|
// Check naming convention (hyphen-case: lowercase with hyphens)
|
||||||
|
if (!/^[a-z0-9-]+$/.test(trimmedName)) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `Name '${trimmedName}' should be hyphen-case (lowercase letters, digits, and hyphens only)`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
trimmedName.startsWith("-") ||
|
||||||
|
trimmedName.endsWith("-") ||
|
||||||
|
trimmedName.includes("--")
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `Name '${trimmedName}' cannot start/end with hyphen or contain consecutive hyphens`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Check name length (max 64 characters)
|
||||||
|
if (trimmedName.length > 64) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `Name is too long (${trimmedName.length} characters). Maximum is 64 characters.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate description
|
||||||
|
const description = frontmatter.description;
|
||||||
|
if (typeof description !== "string") {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `Description must be a string, got ${typeof description}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const trimmedDescription = description.trim();
|
||||||
|
if (trimmedDescription) {
|
||||||
|
// Check for angle brackets
|
||||||
|
if (trimmedDescription.includes("<") || trimmedDescription.includes(">")) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: "Description cannot contain angle brackets (< or >)",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Check description length (max 1024 characters)
|
||||||
|
if (trimmedDescription.length > 1024) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `Description is too long (${trimmedDescription.length} characters). Maximum is 1024 characters.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true, message: "Skill is valid!" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI entry point
|
||||||
|
if (require.main === module) {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
if (args.length !== 1) {
|
||||||
|
console.log("Usage: npx ts-node validate-skill.ts <skill-directory>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { valid, message } = validateSkill(args[0] as string);
|
||||||
|
console.log(message);
|
||||||
|
process.exit(valid ? 0 : 1);
|
||||||
|
}
|
||||||
306
src/tests/skills/skill-creator-scripts.test.ts
Normal file
306
src/tests/skills/skill-creator-scripts.test.ts
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
/**
|
||||||
|
* Tests for the bundled skill-creator scripts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
||||||
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import {
|
||||||
|
initSkill,
|
||||||
|
titleCaseSkillName,
|
||||||
|
} from "../../skills/builtin/skill-creator/scripts/init-skill";
|
||||||
|
import { packageSkill } from "../../skills/builtin/skill-creator/scripts/package-skill";
|
||||||
|
import { validateSkill } from "../../skills/builtin/skill-creator/scripts/validate-skill";
|
||||||
|
|
||||||
|
const TEST_DIR = join(import.meta.dir, ".test-skill-creator");
|
||||||
|
|
||||||
|
describe("validate-skill", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
if (existsSync(TEST_DIR)) {
|
||||||
|
rmSync(TEST_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
mkdirSync(TEST_DIR, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (existsSync(TEST_DIR)) {
|
||||||
|
rmSync(TEST_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("validates a valid skill", () => {
|
||||||
|
const skillDir = join(TEST_DIR, "valid-skill");
|
||||||
|
mkdirSync(skillDir);
|
||||||
|
writeFileSync(
|
||||||
|
join(skillDir, "SKILL.md"),
|
||||||
|
`---
|
||||||
|
name: valid-skill
|
||||||
|
description: A valid test skill
|
||||||
|
---
|
||||||
|
|
||||||
|
# Valid Skill
|
||||||
|
|
||||||
|
This is a valid skill.
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = validateSkill(skillDir);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
expect(result.message).toBe("Skill is valid!");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fails when SKILL.md is missing", () => {
|
||||||
|
const skillDir = join(TEST_DIR, "missing-skill");
|
||||||
|
mkdirSync(skillDir);
|
||||||
|
|
||||||
|
const result = validateSkill(skillDir);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.message).toBe("SKILL.md not found");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fails when frontmatter is missing", () => {
|
||||||
|
const skillDir = join(TEST_DIR, "no-frontmatter");
|
||||||
|
mkdirSync(skillDir);
|
||||||
|
writeFileSync(
|
||||||
|
join(skillDir, "SKILL.md"),
|
||||||
|
`# No Frontmatter
|
||||||
|
|
||||||
|
This skill has no frontmatter.
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = validateSkill(skillDir);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.message).toBe("No YAML frontmatter found");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fails when name is missing", () => {
|
||||||
|
const skillDir = join(TEST_DIR, "no-name");
|
||||||
|
mkdirSync(skillDir);
|
||||||
|
writeFileSync(
|
||||||
|
join(skillDir, "SKILL.md"),
|
||||||
|
`---
|
||||||
|
description: A skill without a name
|
||||||
|
---
|
||||||
|
|
||||||
|
# No Name
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = validateSkill(skillDir);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.message).toBe("Missing 'name' in frontmatter");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fails when description is missing", () => {
|
||||||
|
const skillDir = join(TEST_DIR, "no-description");
|
||||||
|
mkdirSync(skillDir);
|
||||||
|
writeFileSync(
|
||||||
|
join(skillDir, "SKILL.md"),
|
||||||
|
`---
|
||||||
|
name: no-description
|
||||||
|
---
|
||||||
|
|
||||||
|
# No Description
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = validateSkill(skillDir);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.message).toBe("Missing 'description' in frontmatter");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fails when name has invalid characters", () => {
|
||||||
|
const skillDir = join(TEST_DIR, "invalid-name");
|
||||||
|
mkdirSync(skillDir);
|
||||||
|
writeFileSync(
|
||||||
|
join(skillDir, "SKILL.md"),
|
||||||
|
`---
|
||||||
|
name: Invalid_Name
|
||||||
|
description: A skill with invalid name
|
||||||
|
---
|
||||||
|
|
||||||
|
# Invalid Name
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = validateSkill(skillDir);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.message).toContain("should be hyphen-case");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fails when name starts with hyphen", () => {
|
||||||
|
const skillDir = join(TEST_DIR, "hyphen-start");
|
||||||
|
mkdirSync(skillDir);
|
||||||
|
writeFileSync(
|
||||||
|
join(skillDir, "SKILL.md"),
|
||||||
|
`---
|
||||||
|
name: -invalid-start
|
||||||
|
description: A skill with invalid name
|
||||||
|
---
|
||||||
|
|
||||||
|
# Invalid Start
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = validateSkill(skillDir);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.message).toContain("cannot start/end with hyphen");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fails when description contains angle brackets", () => {
|
||||||
|
const skillDir = join(TEST_DIR, "angle-brackets");
|
||||||
|
mkdirSync(skillDir);
|
||||||
|
writeFileSync(
|
||||||
|
join(skillDir, "SKILL.md"),
|
||||||
|
`---
|
||||||
|
name: angle-brackets
|
||||||
|
description: A skill with <invalid> description
|
||||||
|
---
|
||||||
|
|
||||||
|
# Angle Brackets
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = validateSkill(skillDir);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.message).toContain("cannot contain angle brackets");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fails when unexpected frontmatter keys are present", () => {
|
||||||
|
const skillDir = join(TEST_DIR, "unexpected-keys");
|
||||||
|
mkdirSync(skillDir);
|
||||||
|
writeFileSync(
|
||||||
|
join(skillDir, "SKILL.md"),
|
||||||
|
`---
|
||||||
|
name: unexpected-keys
|
||||||
|
description: A skill with unexpected keys
|
||||||
|
author: Someone
|
||||||
|
version: 1.0.0
|
||||||
|
---
|
||||||
|
|
||||||
|
# Unexpected Keys
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = validateSkill(skillDir);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.message).toContain("Unexpected key(s)");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("init-skill", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
if (existsSync(TEST_DIR)) {
|
||||||
|
rmSync(TEST_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
mkdirSync(TEST_DIR, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (existsSync(TEST_DIR)) {
|
||||||
|
rmSync(TEST_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("titleCaseSkillName converts hyphenated names", () => {
|
||||||
|
expect(titleCaseSkillName("my-skill")).toBe("My Skill");
|
||||||
|
expect(titleCaseSkillName("pdf-editor")).toBe("Pdf Editor");
|
||||||
|
expect(titleCaseSkillName("big-query-helper")).toBe("Big Query Helper");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("creates a new skill directory with all files", () => {
|
||||||
|
const result = initSkill("test-skill", TEST_DIR);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(existsSync(join(TEST_DIR, "test-skill"))).toBe(true);
|
||||||
|
expect(existsSync(join(TEST_DIR, "test-skill", "SKILL.md"))).toBe(true);
|
||||||
|
expect(existsSync(join(TEST_DIR, "test-skill", "scripts"))).toBe(true);
|
||||||
|
expect(existsSync(join(TEST_DIR, "test-skill", "references"))).toBe(true);
|
||||||
|
expect(existsSync(join(TEST_DIR, "test-skill", "assets"))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("created skill passes validation", () => {
|
||||||
|
initSkill("valid-init", TEST_DIR);
|
||||||
|
|
||||||
|
// The initialized skill should pass validation (except for TODO in description)
|
||||||
|
const skillDir = join(TEST_DIR, "valid-init");
|
||||||
|
expect(existsSync(join(skillDir, "SKILL.md"))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fails when directory already exists", () => {
|
||||||
|
const skillDir = join(TEST_DIR, "existing-skill");
|
||||||
|
mkdirSync(skillDir, { recursive: true });
|
||||||
|
|
||||||
|
const result = initSkill("existing-skill", TEST_DIR);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("package-skill", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
if (existsSync(TEST_DIR)) {
|
||||||
|
rmSync(TEST_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
mkdirSync(TEST_DIR, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (existsSync(TEST_DIR)) {
|
||||||
|
rmSync(TEST_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("packages a valid skill into a .skill file", () => {
|
||||||
|
// Create a valid skill
|
||||||
|
const skillDir = join(TEST_DIR, "packagable-skill");
|
||||||
|
mkdirSync(skillDir);
|
||||||
|
writeFileSync(
|
||||||
|
join(skillDir, "SKILL.md"),
|
||||||
|
`---
|
||||||
|
name: packagable-skill
|
||||||
|
description: A skill that can be packaged
|
||||||
|
---
|
||||||
|
|
||||||
|
# Packagable Skill
|
||||||
|
|
||||||
|
This skill can be packaged.
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = packageSkill(skillDir, TEST_DIR);
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(existsSync(join(TEST_DIR, "packagable-skill.skill"))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fails when skill directory does not exist", () => {
|
||||||
|
const result = packageSkill(join(TEST_DIR, "nonexistent"), TEST_DIR);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fails when SKILL.md is missing", () => {
|
||||||
|
const skillDir = join(TEST_DIR, "no-skill-md");
|
||||||
|
mkdirSync(skillDir);
|
||||||
|
|
||||||
|
const result = packageSkill(skillDir, TEST_DIR);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("fails when skill validation fails", () => {
|
||||||
|
const skillDir = join(TEST_DIR, "invalid-skill");
|
||||||
|
mkdirSync(skillDir);
|
||||||
|
writeFileSync(
|
||||||
|
join(skillDir, "SKILL.md"),
|
||||||
|
`---
|
||||||
|
name: Invalid_Name
|
||||||
|
description: Invalid skill
|
||||||
|
---
|
||||||
|
|
||||||
|
# Invalid
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = packageSkill(skillDir, TEST_DIR);
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -108,11 +108,15 @@ async function readSkillContent(
|
|||||||
skillId: string,
|
skillId: string,
|
||||||
skillsDir: string,
|
skillsDir: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// 1. Check bundled skills first (they have embedded content)
|
// 1. Check bundled skills first (they have a path now)
|
||||||
const bundledSkills = getBundledSkills();
|
const bundledSkills = await getBundledSkills();
|
||||||
const bundledSkill = bundledSkills.find((s) => s.id === skillId);
|
const bundledSkill = bundledSkills.find((s) => s.id === skillId);
|
||||||
if (bundledSkill?.content) {
|
if (bundledSkill?.path) {
|
||||||
return bundledSkill.content;
|
try {
|
||||||
|
return await readFile(bundledSkill.path, "utf-8");
|
||||||
|
} catch {
|
||||||
|
// Bundled skill path not found, continue to other sources
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Try global skills directory
|
// 2. Try global skills directory
|
||||||
|
|||||||
@@ -27,5 +27,6 @@
|
|||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"noPropertyAccessFromIndexSignature": false
|
"noPropertyAccessFromIndexSignature": false
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"]
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["src/skills/builtin/**/scripts/**"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user