Files
CherryHQ-cherry-studio/resources/skills/skill-creator/scripts/package_skill.py
LiuVaayne 7d0e066a9a feat(agents): add CherryClaw autonomous agent type (#13359)
### What this PR does

Before this PR:
Cherry Studio only supports `claude-code` as an agent type. Agents have
no autonomous scheduling, no IM channel integration, and no
soul/personality system.

After this PR:
Introduces **CherryClaw** — a new autonomous agent type with:
- **Soul-driven personality**: Markdown-based soul files with
mtime-cached reading
- **Task-based scheduler**: Poll-loop scheduler with drift-resistant
interval computation, tasks as first-class DB entities
(nanoclaw-inspired)
- **Internal claw MCP server**: `cron` tool (add/list/remove)
auto-injected into CherryClaw sessions for autonomous task management
- **Channel abstraction layer**: Pluggable adapter pattern with Telegram
as the first implementation (grammY, long polling, streaming drafts,
typing indicators)
- **Headless message persistence**: Channel and scheduler messages now
persist to the agent SQLite DB
- **Basic sandbox mode**: PreToolUse hook path enforcement + OS-level
sandbox toggle
- **Full UI**: Agent creation modal with type selector, settings tabs
(soul, tasks, channels, advanced), task management CRUD, channel catalog
with inline config
- **53 unit tests** across 8 test files covering all new services

<!-- Fixes # -->

### Why we need it and why it was done in this way

CherryClaw enables Cherry Studio agents to operate autonomously —
executing scheduled tasks and responding to IM messages without user
interaction. This is the foundation for "always-on" AI assistants.

The following tradeoffs were made:
- **Poll-loop scheduler over timer-based**: DB is the source of truth;
no timer state to restore on restart. Simpler, more robust at the cost
of up to 60s latency.
- **AgentServiceRegistry pattern**: Replaced hardcoded
`ClaudeCodeService` in `SessionMessageService` with a registry mapping
`AgentType` → service. Extensible for future agent types.
- **Internal MCP server for cron**: Rather than extending the SDK's tool
system, the `cron` tool is served as a standard MCP server at
`/v1/claw/:agentId/claw-mcp`. This lets the agent discover and use it
naturally.
- **Channel abstraction over direct Telegram integration**:
`ChannelAdapter` + factory registration enables future Discord/Slack
adapters without touching core routing logic.
- **Basic sandbox (not security boundary)**: PreToolUse hook + OS
sandbox provides best-effort restriction for well-behaved agents. Known
bypass vectors documented; hardening deferred.

The following alternatives were considered:
- cron-based OS scheduling (rejected: harder to manage lifecycle, no DB
integration)
- Direct Telegram bot API calls (rejected: grammY provides typed API,
connection management, and middleware)
- Modifying SDK builtin tools (rejected: internal MCP server is cleaner
separation)

### Breaking changes

None. This is a new agent type (`cherry-claw`) alongside the existing
`claude-code` type. No existing behavior is modified.

### Special notes for your reviewer

- **New DB migration**: `0003_wise_meltdown.sql` adds `scheduled_tasks`
and `task_run_logs` tables (agents DB only, not IndexedDB)
- **New dependencies**: `cron-parser` ^5.5.0, `grammy` ^1.41
- **Placeholder avatar**: `cherry-claw.png` is currently a copy of
`claude.png` — needs a proper distinct image
- **74 files changed, ~7400 lines added** — large PR, recommend
reviewing by phase (type system → backend services → MCP → channels → UI
→ tests)
- **Sandbox is basic only**: The PreToolUse path checking has known
bypasses (relative paths, variable expansion). Documented in handoff.md.
Hardening is follow-up work.
- The `handoff.md` file in the repo root contains full architectural
context and decisions

### Checklist

- [x] PR: The PR description is expressive enough and will help future
contributors
- [x] Code: [Write code that humans can
understand](https://en.wikiquote.org/wiki/Martin_Fowler#code-for-humans)
and [Keep it simple](https://en.wikipedia.org/wiki/KISS_principle)
- [x] Refactor: You have [left the code cleaner than you found it (Boy
Scout
Rule)](https://learning.oreilly.com/library/view/97-things-every/9780596809515/ch08.html)
- [ ] Upgrade: Impact of this change on upgrade flows was considered and
addressed if required
- [ ] Documentation: A [user-guide update](https://docs.cherry-ai.com)
was considered and is present (link) or not required. Check this only
when the PR introduces or changes a user-facing feature or behavior.
- [ ] Self-review: I have reviewed my own code (e.g., via
[`/gh-pr-review`](/.claude/skills/gh-pr-review/SKILL.md), `gh pr diff`,
or GitHub UI) before requesting review from others

### Release note

```release-note
New CherryClaw agent type: autonomous agents with soul-driven personality, task-based scheduling (cron/interval/one-time), internal cron MCP tool for self-managed tasks, Telegram channel integration with streaming responses, and basic sandbox mode for filesystem restriction.
```

---------

Signed-off-by: Vaayne <liu.vaayne@gmail.com>
Signed-off-by: suyao <sy20010504@gmail.com>
Signed-off-by: Siin Xu <31815270+SiinXu@users.noreply.github.com>
Signed-off-by: zhangjiadi225 <625013594@qq.com>
Signed-off-by: greycheng255 <greycheng255@gmail.com>
Co-authored-by: kangfenmao <kangfenmao@qq.com>
Co-authored-by: suyao <sy20010504@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Siin Xu <31815270+SiinXu@users.noreply.github.com>
Co-authored-by: fullex <106392080+0xfullex@users.noreply.github.com>
Co-authored-by: zhangjiadi225 <625013594@qq.com>
2026-04-02 19:58:34 +08:00

137 lines
4.1 KiB
Python
Executable File

#!/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 fnmatch
import sys
import zipfile
from pathlib import Path
from scripts.quick_validate import validate_skill
# Patterns to exclude when packaging skills.
EXCLUDE_DIRS = {"__pycache__", "node_modules"}
EXCLUDE_GLOBS = {"*.pyc"}
EXCLUDE_FILES = {".DS_Store"}
# Directories excluded only at the skill root (not when nested deeper).
ROOT_EXCLUDE_DIRS = {"evals"}
def should_exclude(rel_path: Path) -> bool:
"""Check if a path should be excluded from packaging."""
parts = rel_path.parts
if any(part in EXCLUDE_DIRS for part in parts):
return True
# rel_path is relative to skill_path.parent, so parts[0] is the skill
# folder name and parts[1] (if present) is the first subdir.
if len(parts) > 1 and parts[1] in ROOT_EXCLUDE_DIRS:
return True
name = rel_path.name
if name in EXCLUDE_FILES:
return True
return any(fnmatch.fnmatch(name, pat) for pat in EXCLUDE_GLOBS)
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, excluding build artifacts
for file_path in skill_path.rglob('*'):
if not file_path.is_file():
continue
arcname = file_path.relative_to(skill_path.parent)
if should_exclude(arcname):
print(f" Skipped: {arcname}")
continue
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()