Skip to content

Commit 971310a

Browse files
committed
SKILL.md working - tested with smiley creator gif
1 parent 5de0695 commit 971310a

File tree

9 files changed

+645
-1
lines changed

9 files changed

+645
-1
lines changed

Dockerfile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
4141
libgbm1 \
4242
libxshmfence1 \
4343
libasound2 \
44+
unzip \
45+
p7zip-full \
46+
bc \
47+
ripgrep \
48+
fd-find \
49+
sqlite3 \
50+
libsqlite3-dev \
51+
wkhtmltopdf \
52+
poppler-utils \
53+
default-jre \
4454
&& apt-get clean && rm -rf /var/lib/apt/lists/*
4555

4656

@@ -64,6 +74,12 @@ COPY ./server.py /app/server.py
6474
# Create application/jupyter directories
6575
RUN mkdir -p /app/uploads /app/jupyter_runtime
6676

77+
# Copy skills directory structure into the container
78+
# Public skills are baked into the image
79+
# User skills directory is created as mount point for user-added skills
80+
COPY ./skills/public /app/uploads/skills/public
81+
RUN mkdir -p /app/uploads/skills/user
82+
6783
# # Generate SSH host keys
6884
# RUN ssh-keygen -A
6985

README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,11 +178,77 @@ Code runs in an isolated container with VM-level isolation. Your host system and
178178
From [@apple/container](https://github.com/apple/container/blob/main/docs/technical-overview.md):
179179
>Each container has the isolation properties of a full VM, using a minimal set of core utilities and dynamic libraries to reduce resource utilization and attack surface.
180180
181+
## Skills System
182+
183+
CodeRunner includes a built-in skills system that provides pre-packaged tools for common tasks. Skills are organized into two categories:
184+
185+
### Built-in Public Skills
186+
187+
The following skills are included in every CodeRunner installation:
188+
189+
- **pdf-text-replace** - Replace text in fillable PDF forms
190+
- **image-crop-rotate** - Crop and rotate images
191+
192+
### Using Skills
193+
194+
Skills are accessed through MCP tools:
195+
196+
```python
197+
# List all available skills
198+
result = await list_skills()
199+
200+
# Get documentation for a specific skill
201+
info = await get_skill_info("pdf-text-replace")
202+
203+
# Execute a skill's script
204+
code = """
205+
import subprocess
206+
subprocess.run([
207+
'python',
208+
'/app/uploads/skills/public/pdf-text-replace/scripts/replace_text_in_pdf.py',
209+
'/app/uploads/input.pdf',
210+
'OLD TEXT',
211+
'NEW TEXT',
212+
'/app/uploads/output.pdf'
213+
])
214+
"""
215+
result = await execute_python_code(code)
216+
```
217+
218+
### Adding Custom Skills
219+
220+
Users can add their own skills to the `~/.coderunner/assets/skills/user/` directory:
221+
222+
1. Create a directory for your skill (e.g., `my-custom-skill/`)
223+
2. Add a `SKILL.md` file with documentation
224+
3. Add your scripts in a `scripts/` subdirectory
225+
4. Skills will be automatically discovered by the `list_skills()` tool
226+
227+
**Skill Structure:**
228+
```
229+
~/.coderunner/assets/skills/user/my-custom-skill/
230+
├── SKILL.md # Documentation with usage examples
231+
└── scripts/ # Your Python/bash scripts
232+
└── process.py
233+
```
234+
235+
### Example: Using the PDF Text Replace Skill
236+
237+
```bash
238+
# Inside the container, execute:
239+
python /app/uploads/skills/public/pdf-text-replace/scripts/replace_text_in_pdf.py \
240+
/app/uploads/tax_form.pdf \
241+
"John Doe" \
242+
"Jane Smith" \
243+
/app/uploads/tax_form_updated.pdf
244+
```
245+
181246
## Architecture
182247

183248
CodeRunner consists of:
184249
- **Sandbox Container:** Isolated execution environment with Jupyter kernel
185250
- **MCP Server:** Handles communication between AI models and the sandbox
251+
- **Skills System:** Pre-packaged tools for common tasks (PDF manipulation, image processing, etc.)
186252

187253
## Examples
188254

install.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ else
1919
echo "✅ macOS system detected."
2020
fi
2121

22-
download_url="https://github.com/apple/container/releases/download/0.3.0/container-0.3.0-installer-signed.pkg"
22+
download_url="https://github.com/apple/container/releases/download/0.5.0/container-0.5.0-installer-signed.pkg"
2323

2424
# Check if container is installed and display its version
2525
if command -v container &> /dev/null

requirements.txt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,44 @@ fastmcp
3838
openai-agents
3939

4040
playwright==1.53.0
41+
42+
# Data Science Libraries
43+
pandas
44+
numpy
45+
scipy
46+
scikit-learn
47+
statsmodels
48+
49+
# Visualization Libraries
50+
matplotlib
51+
seaborn
52+
53+
# File Processing Libraries
54+
pyarrow
55+
openpyxl
56+
xlsxwriter
57+
xlrd
58+
pillow
59+
python-pptx
60+
python-docx
61+
pypdf
62+
pdfplumber
63+
pypdfium2
64+
pdf2image
65+
pdfkit
66+
tabula-py
67+
reportlab[pycairo]
68+
img2pdf
69+
70+
# Math & Computing Libraries
71+
sympy
72+
mpmath
73+
74+
# Utilities
75+
tqdm
76+
python-dateutil
77+
pytz
78+
joblib
79+
80+
# Beautiful Soup for HTML parsing (already used in server.py)
81+
beautifulsoup4

server.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import json
66
import logging
77
import os
8+
import zipfile
89
import pathlib
910
import time
1011
import uuid
@@ -53,6 +54,11 @@
5354
SHARED_DIR.mkdir(exist_ok=True)
5455
KERNEL_ID_FILE_PATH = SHARED_DIR / "python_kernel_id.txt"
5556

57+
# Skills directory configuration
58+
SKILLS_DIR = SHARED_DIR / "skills"
59+
PUBLIC_SKILLS_DIR = SKILLS_DIR / "public"
60+
USER_SKILLS_DIR = SKILLS_DIR / "user"
61+
5662
def resolve_with_system_dns(hostname):
5763
try:
5864
return socket.gethostbyname(hostname)
@@ -530,5 +536,149 @@ async def navigate_and_get_all_visible_text(url: str) -> str:
530536
return f"Error: Failed to retrieve all visible text: {str(e)}"
531537

532538

539+
# --- SKILLS MANAGEMENT TOOLS ---
540+
541+
542+
async def _parse_skill_frontmatter(skill_md_path):
543+
try:
544+
async with aiofiles.open(skill_md_path, mode='r') as f:
545+
content = await f.read()
546+
frontmatter = []
547+
in_frontmatter = False
548+
for line in content.splitlines():
549+
if line.strip() == '---':
550+
if in_frontmatter:
551+
break
552+
else:
553+
in_frontmatter = True
554+
continue
555+
if in_frontmatter:
556+
frontmatter.append(line)
557+
558+
metadata = {}
559+
for line in frontmatter:
560+
if ':' in line:
561+
key, value = line.split(':', 1)
562+
metadata[key.strip()] = value.strip()
563+
return metadata
564+
except Exception:
565+
return {}
566+
567+
@mcp.tool()
568+
async def list_skills() -> str:
569+
"""
570+
Lists all available skills in the CodeRunner container.
571+
572+
Returns a list of available skills organized by category (public/user).
573+
Public skills are built into the container, while user skills are added by users.
574+
575+
Returns:
576+
JSON string with skill names organized by category.
577+
"""
578+
try:
579+
# Unzip any user-provided skills
580+
if USER_SKILLS_DIR.exists():
581+
for item in USER_SKILLS_DIR.iterdir():
582+
if item.is_file() and item.suffix == '.zip':
583+
with zipfile.ZipFile(item, 'r') as zip_ref:
584+
zip_ref.extractall(USER_SKILLS_DIR)
585+
os.remove(item)
586+
587+
skills = {
588+
"public": [],
589+
"user": []
590+
}
591+
592+
# Helper to process a skills directory
593+
async def process_skill_dir(directory, category):
594+
if directory.exists():
595+
for skill_dir in directory.iterdir():
596+
if skill_dir.is_dir():
597+
skill_md_path = skill_dir / "SKILL.md"
598+
if skill_md_path.exists():
599+
metadata = await _parse_skill_frontmatter(skill_md_path)
600+
skills[category].append({
601+
"name": metadata.get("name", skill_dir.name),
602+
"description": metadata.get("description", "No description available.")
603+
})
604+
605+
await process_skill_dir(PUBLIC_SKILLS_DIR, "public")
606+
await process_skill_dir(USER_SKILLS_DIR, "user")
607+
608+
# Sort for consistent output
609+
skills["public"].sort(key=lambda x: x['name'])
610+
skills["user"].sort(key=lambda x: x['name'])
611+
612+
result = f"Available Skills:\n\n"
613+
result += f"Public Skills ({len(skills['public'])}):\n"
614+
if skills["public"]:
615+
for skill in skills["public"]:
616+
result += f" - {skill['name']}: {skill['description']}\n"
617+
else:
618+
result += " (none)\n"
619+
620+
result += f"\nUser Skills ({len(skills['user'])}):\n"
621+
if skills["user"]:
622+
for skill in skills["user"]:
623+
result += f" - {skill['name']}: {skill['description']}\n"
624+
else:
625+
result += " (none)\n"
626+
627+
result += f"\nUse get_skill_info(skill_name) to read documentation for a specific skill."
628+
629+
return result
630+
631+
except Exception as e:
632+
logger.error(f"Failed to list skills: {e}")
633+
return f"Error: Failed to list skills: {str(e)}"
634+
635+
636+
@mcp.tool()
637+
async def get_skill_info(skill_name: str) -> str:
638+
"""
639+
Retrieves the documentation (SKILL.md) for a specific skill.
640+
641+
Args:
642+
skill_name: The name of the skill (e.g., 'pdf-text-replace', 'image-crop-rotate')
643+
644+
Returns:
645+
The content of the skill's SKILL.md file with usage instructions and examples.
646+
"""
647+
try:
648+
# Check public skills first
649+
public_skill_path = PUBLIC_SKILLS_DIR / skill_name / "SKILL.md"
650+
user_skill_path = USER_SKILLS_DIR / skill_name / "SKILL.md"
651+
652+
skill_path = None
653+
skill_type = None
654+
655+
if public_skill_path.exists():
656+
skill_path = public_skill_path
657+
skill_type = "public"
658+
elif user_skill_path.exists():
659+
skill_path = user_skill_path
660+
skill_type = "user"
661+
else:
662+
return f"Error: Skill '{skill_name}' not found. Use list_skills() to see available skills."
663+
664+
# Read the SKILL.md content
665+
async with aiofiles.open(skill_path, mode='r') as f:
666+
content = await f.read()
667+
668+
# Replace all occurrences of /mnt/user-data with /app/uploads
669+
content = content.replace('/mnt/user-data', '/app/uploads')
670+
671+
# Add header with skill type
672+
header = f"Skill: {skill_name} ({skill_type})\n"
673+
header += f"Location: /app/uploads/skills/{skill_type}/{skill_name}/\n"
674+
header += "=" * 80 + "\n\n"
675+
676+
return header + content
677+
678+
except Exception as e:
679+
logger.error(f"Failed to get skill info for '{skill_name}': {e}")
680+
return f"Error: Failed to get skill info: {str(e)}"
681+
682+
533683
# Use the streamable_http_app as it's the modern standard
534684
app = mcp.streamable_http_app()

0 commit comments

Comments
 (0)