Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions agent_reach/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,11 +424,12 @@ def _copy_skill_dir(target: str) -> str | None:
print(f" Warning: Could not install skill: {e}")
return None

# Determine skill install path (priority: .agents > openclaw > claude)
# Determine skill install path (priority: .agents > opencode > openclaw > claude)
skill_dirs = [
os.path.expanduser("~/.agents/skills"), # Generic agents (priority)
os.path.expanduser("~/.openclaw/skills"), # OpenClaw
os.path.expanduser("~/.claude/skills"), # Claude Code (if exists)
os.path.expanduser("~/.agents/skills"), # Generic agents (priority)
os.path.expanduser("~/.config/opencode/skills"), # OpenCode
os.path.expanduser("~/.openclaw/skills"), # OpenClaw
os.path.expanduser("~/.claude/skills"), # Claude Code (if exists)
]

# Insert OPENCLAW_HOME path at the beginning if environment variable is set
Expand All @@ -442,7 +443,12 @@ def _copy_skill_dir(target: str) -> str | None:
target = os.path.join(skill_dir, "agent-reach")
status = _copy_skill_dir(target)
if status:
platform_name = "Agent" if ".agents" in skill_dir else "OpenClaw" if "openclaw" in skill_dir else "Claude Code"
platform_name = (
"OpenCode" if "opencode" in skill_dir
else "Agent" if ".agents" in skill_dir
else "OpenClaw" if "openclaw" in skill_dir
else "Claude Code"
)
if status == "preserved":
print(f"Skill already installed for {platform_name}, preserving existing files: {target}")
else:
Expand All @@ -460,7 +466,7 @@ def _copy_skill_dir(target: str) -> str | None:
print(f"Skill installed: {target}")
else:
print(" -- Could not install agent skill (optional)")
print(" -- Tip: install OpenClaw, Claude Code, or create ~/.agents/skills/ manually")
print(" -- Tip: install OpenClaw, OpenCode, Claude Code, or create ~/.agents/skills/ manually")


def _uninstall_skill():
Expand All @@ -469,6 +475,7 @@ def _uninstall_skill():

skill_dirs = [
("~/.openclaw/skills/agent-reach", "OpenClaw"),
("~/.config/opencode/skills/agent-reach", "OpenCode"),
("~/.claude/skills/agent-reach", "Claude Code"),
("~/.agents/skills/agent-reach", "Agent"),
]
Expand Down Expand Up @@ -1412,6 +1419,7 @@ def _cmd_uninstall(args):
# ── 2. Skill files ──
skill_dirs = [
("~/.openclaw/skills/agent-reach", "OpenClaw"),
("~/.config/opencode/skills/agent-reach", "OpenCode"),
("~/.claude/skills/agent-reach", "Claude Code"),
("~/.agents/skills/agent-reach", "Agent"),
]
Expand Down
37 changes: 19 additions & 18 deletions agent_reach/skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,32 @@ description: >

【路由方式】SKILL.md 包含路由表和常用命令,复杂场景需按需阅读对应分类的 references/*.md。
分类:search / social (小红书/推特/B站/V2EX/Reddit/Facebook/Instagram) / career(LinkedIn) / dev(github) / web(网页/文章/RSS) / video(YouTube/B站/播客)。
triggers:
- research: 调研/全网调研/帮我调研/研究一下/research/深入了解
- search: 搜/查/找/search/搜索/查一下/帮我搜/看看大家怎么说
- social:
- 小红书: xiaohongshu/xhs/小红书/红书
- Twitter: twitter/推特/x.com/推文
- B站: bilibili/b站/哔哩哔哩
- V2EX: v2ex
- Reddit: reddit
- Facebook: facebook/fb/facebook groups
- Instagram: instagram/ig
- career: 招聘/职位/求职/linkedin/领英/找工作
- dev: github/代码/仓库/gh/issue/pr/分支/commit
- web: 网页/链接/文章/rss/读一下/打开这个
- video: youtube/视频/播客/字幕/小宇宙/转录/yt
- finance: 雪球/股票/stock/xueqiu/行情/基金
metadata:
openclaw:
homepage: https://github.com/Panniantong/Agent-Reach
homepage: https://github.com/Panniantong/Agent-Reach
---

# Agent Reach — 互联网能力路由器

15 平台、多后端。**本 skill 存在时必须用它访问这些平台,不要自己发明方案。**

## 触发关键词(Triggers)

- research: 调研/全网调研/帮我调研/研究一下/research/深入了解
- search: 搜/查/找/search/搜索/查一下/帮我搜/看看大家怎么说
- social:
- 小红书: xiaohongshu/xhs/小红书/红书
- Twitter: twitter/推特/x.com/推文
- B站: bilibili/b站/哔哩哔哩
- V2EX: v2ex
- Reddit: reddit
- Facebook: facebook/fb/facebook groups
- Instagram: instagram/ig
- career: 招聘/职位/求职/linkedin/领英/找工作
- dev: github/代码/仓库/gh/issue/pr/分支/commit
- web: 网页/链接/文章/rss/读一下/打开这个
- video: youtube/视频/播客/字幕/小宇宙/转录/yt
- finance: 雪球/股票/stock/xueqiu/行情/基金

## 常驻规则(全程适用)

1. **动手前先体检**:多后端/登录态平台(小红书/Reddit/B站/Twitter/Facebook/Instagram)先跑
Expand Down
3 changes: 1 addition & 2 deletions agent_reach/skill/SKILL_en.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ description: >
internet content); posting/commenting/liking (write operations); platforms
that already have a dedicated skill installed (prefer that skill).
metadata:
openclaw:
homepage: https://github.com/Panniantong/Agent-Reach
homepage: https://github.com/Panniantong/Agent-Reach
---

# Agent Reach — internet capability router
Expand Down
67 changes: 67 additions & 0 deletions tests/test_skill_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,83 @@

import importlib.resources
import os
import re
import tempfile
import unittest
from unittest.mock import patch

import yaml

from agent_reach.cli import _install_skill, _uninstall_skill

# OpenCode only recognizes these frontmatter keys (see opencode.ai/docs/skills).
OPENCODE_ALLOWED_FRONTMATTER_KEYS = frozenset(
{"name", "description", "license", "compatibility", "metadata"}
)


def _read_skill_frontmatter(resource_name: str) -> dict:
skill_dir = importlib.resources.files("agent_reach").joinpath("skill")
text = skill_dir.joinpath(resource_name).read_text(encoding="utf-8")
match = re.match(r"^---\n(.*?)\n---", text, re.DOTALL)
assert match, f"{resource_name} must start with YAML frontmatter"
return yaml.safe_load(match.group(1))


class TestSkillCommand(unittest.TestCase):
"""Test skill install and uninstall via CLI helpers."""

def test_skill_frontmatter_is_opencode_compatible(self):
"""Frontmatter should only use fields recognized by OpenCode agents."""
for resource_name in ("SKILL.md", "SKILL_en.md"):
with self.subTest(resource=resource_name):
frontmatter = _read_skill_frontmatter(resource_name)
unknown_keys = set(frontmatter) - OPENCODE_ALLOWED_FRONTMATTER_KEYS
self.assertEqual(
unknown_keys,
set(),
f"Unsupported frontmatter keys in {resource_name}: {unknown_keys}",
)
metadata = frontmatter.get("metadata")
if metadata is not None:
self.assertIsInstance(metadata, dict)
for key, value in metadata.items():
self.assertIsInstance(
key,
str,
f"metadata key must be a string in {resource_name}",
)
self.assertIsInstance(
value,
str,
f"metadata[{key!r}] must be a string in {resource_name}",
)
description = frontmatter.get("description", "")
self.assertGreaterEqual(len(description), 1)
self.assertLessEqual(len(description), 1024)

def test_install_skill_to_opencode_directory(self):
"""_install_skill should install to ~/.config/opencode/skills when present."""
with tempfile.TemporaryDirectory() as tmpdir:
skill_parent = os.path.join(tmpdir, ".config", "opencode", "skills")
os.makedirs(skill_parent)

with patch(
"agent_reach.cli.os.path.expanduser",
side_effect=lambda p: p.replace("~", tmpdir),
):
env = os.environ.copy()
env.pop("OPENCLAW_HOME", None)
with patch.dict(os.environ, env, clear=True):
_install_skill()

target = os.path.join(skill_parent, "agent-reach", "SKILL.md")
self.assertTrue(os.path.exists(target))
with open(target, encoding="utf-8") as f:
content = f.read()
self.assertIn("Agent Reach", content)
self.assertNotIn("\ntriggers:", content[: content.index("\n# ")])

def test_skill_resources_include_both_locales(self):
"""Package resources should expose both default and English skill markdown files."""
skill_dir = importlib.resources.files("agent_reach").joinpath("skill")
Expand Down