@clawhub-goog-71bd270d77
Knowledge Card generator. Extracts key knowledge from user-provided material (text, files, URLs), determines optimal card type (concept/memo/process/comparis...
---
name: kcard
description: Knowledge Card generator. Extracts key knowledge from user-provided material (text, files, URLs), determines optimal card type (concept/memo/process/comparison), applies cognitive science principles (chunking, dual coding, elaboration), outputs structured Markdown, and renders it into a beautiful image. Use when user says "知识卡片", "kcard", "make a card", "knowledge card", or wants to turn notes/articles into memorable visual cards.
---
# Knowledge Card Generator
## Workflow
### 1. Parse Input Material
Accept any of: pasted text, file path, URL, or image.
- If URL → fetch and extract main content by `web_fetch` tool
- If file → read it
Extract 3–7 core knowledge points. Prioritize: definitions > mechanisms > examples > details.
### 2. Determine Card Type
Pick the **best-fit** type based on content nature:
| Type | Trigger Pattern | Structure |
|------|----------------|-----------|
| **Concept** | Defines a term, theory, model | Term → Definition → Analogy → Key Points |
| **Memo** | Steps, commands, configs, references | Title → Ordered Steps → Tips / Gotchas |
| **Process** | Sequential workflow or lifecycle | Title → Phases → Steps per Phase → Output |
| **Comparison** | Compares 2+ items | Dimension → Item A vs Item B → Verdict |
If unsure, default to **Concept card**.
### 3. Apply Cognitive Science Principles
Follow these when structuring the card:
- **Chunking**: Group related info into 3–5 chunks max per section
- **Dual Coding**: Pair text with a visual metaphor or emoji anchors
- **Elaboration**: Add a "Why It Matters" or analogy section
- **Spaced Repetition Cue**: End with a self-test question (❓)
- **Progressive Disclosure**: Layer from simple to detailed
### 4. Generate Markdown
Use the template from `references/card-templates.md`. Output a single Markdown file.
Naming convention: `kcard_<topic>_<type>.md` (e.g., `kcard_react-hooks_concept.md`)
Save to user's specified path or default: `~/.openclaw/workspace/kcards/`
### 5. Render to Image
Run the rendering script to convert the Markdown into a PNG:
```bash
python <skill-dir>/scripts/render_card.py <path-to-markdown> [--output <output.png>] [--theme <warm|cool|girly|tech>] [--width 800]
```
Default theme: `warm`. Default output: same path with `.png` extension.
The script:
1. Parses Markdown to styled HTML
2. Renders HTML to image via headless browser or html2image
3. Returns the output path
Present the final image to the user.
## Output Format
Always output:
1. The Markdown source file (for editing/reuse)
2. The rendered PNG image
3. A brief one-line summary of what the card covers
## Notes
- Keep cards concise: one concept per card, maximum 195 words
- Use Chinese or English based on input language
- Emoji anchors are encouraged but keep them minimal (1–3 per section)
- For batch requests, process cards sequentially and summarize all outputs
FILE:scripts/render_card.py
#!/usr/bin/env python3
"""
Knowledge Card Renderer - Convert Markdown to beautiful card images.
Dependencies: pip install markdown html2image pillow
Optional: pip install Pygments (for syntax highlighting)
"""
import argparse
import os
import sys
import tempfile
from pathlib import Path
from PIL import Image
THEMES = {
"warm": {
"bg": "#FFF8F0",
"card": "#FDBA74",
"accent": "#E8734A",
"accent_light": "#FFF0E8",
"text": "#2D2D2D",
"text_secondary": "#6B6B6B",
"border": "#E8DDD4",
"code_bg": "#F5EDE5",
"shadow": "0 4px 12px rgba(0,0,0,0.1)",
},
"cool": {
"bg": "#F0F4F8",
"card": "white",
"accent": "#3B82F6",
"accent_light": "#EBF4FF",
"text": "#1E293B",
"text_secondary": "#64748B",
"border": "#CBD5E1",
"code_bg": "#E2E8F0",
},
"minimal": {
"bg": "#FFFFFF",
"card": "white",
"accent": "#333333",
"accent_light": "#F5F5F5",
"text": "#1A1A1A",
"text_secondary": "#777777",
"border": "#E5E5E5",
"code_bg": "#F9F9F9",
},
"girly": {
"bg": "#FFF0F5", # 淡粉背景
"card": "white",
"accent": "#FF6FA5", # 玫粉主色
"accent_light": "#FFE3EC", # 浅粉辅助
"text": "#4A2C2A", # 温柔棕色文字
"text_secondary": "#A67C8A", # 灰粉次级文字
"border": "#FFD1DC", # 粉色边框
"code_bg": "#FFF7FA", # 很浅的粉背景
},
"metal": {
"bg": "#0A1F33",
"card": "#2A3440",
"accent": "#8FA3B8", # 冷钢蓝主色
"accent_light": "#2A3440", # 深灰金属辅助
"text": "#E6EDF3", # 银白文字
"text_secondary": "#9AA4AE", # 冷灰次级文字
"border": "#3B4752", # 金属灰边框
"code_bg": "#161B22", # 深色代码背景
},
"tech": {
# 背景层
"bg": "#020617", # 更深的黑蓝(拉开层级)
"card": "#0B1220", # 卡片背景(明显区分)
"section": "#111827", # 模块块背景
"accent_light": "#111827",
# 主色系统
"accent": "#3B82F6", # 主蓝(更稳)
"highlight": "#22D3EE", # 科技青(关键点缀🔥)
"glow": "#60A5FA", # 柔和发光
# 文本
"text": "#F1F5F9", # 提亮(更清晰)
"text_secondary": "#94A3B8",
# 边框 / 分割
"border": "#1F2937", # 更暗一点,避免抢眼
# 特殊区域
"code_bg": "#020617", # 直接统一更干净
}
}
CSS_TEMPLATE = """
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&family=Inter:wght@300;400;500;600;700&display=swap');
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: 'Inter', 'Noto Sans SC', -apple-system, BlinkMacSystemFont, sans-serif;
background: {bg};
color: {text};
padding: 40px;
line-height: 1.7;
-webkit-font-smoothing: antialiased;
}}
.card {{
max-width: {width}px;
margin: 0 auto;
background: {card};
border-radius: 16px;
padding: 40px 48px;
box-shadow: 0 4px 24px rgba(0,0,0,0.06), 0 1px 4px rgba(0,0,0,0.04);
border: 1px solid {border};
}}
h1 {{
font-size: 28px;
font-weight: 700;
color: {accent};
text-shadow: 0 0 12px rgba(59,130,246,0.4);
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 3px solid {accent};
letter-spacing: -0.5px;
}}
h2 {{
font-size: 18px;
font-weight: 600;
color: {text};
margin-top: 28px;
margin-bottom: 12px;
padding-left: 12px;
border-left: 4px solid {accent};
}}
h3 {{
font-size: 15px;
font-weight: 600;
color: {text_secondary};
margin-top: 20px;
margin-bottom: 8px;
}}
p {{
margin-bottom: 12px;
font-size: 15px;
color: {text};
}}
blockquote {{
background: {accent_light};
border-left: 4px solid {accent};
margin: 16px 0;
padding: 14px 20px;
border-radius: 0 8px 8px 0;
font-size: 15px;
color: {text};
}}
blockquote p {{
margin-bottom: 0;
}}
ul, ol {{
margin: 12px 0;
padding-left: 24px;
}}
li {{
margin-bottom: 8px;
font-size: 15px;
}}
li strong {{
color: {accent};
}}
table {{
width: 100%;
border-collapse: collapse;
margin: 16px 0;
font-size: 14px;
border-radius: 8px;
overflow: hidden;
border: 1px dashed {border};
}}
thead {{
background: {accent};
color: white;
}}
th {{
padding: 12px 16px;
text-align: left;
font-weight: 600;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.5px;
}}
td {{
padding: 10px 16px;
border-bottom: 1px solid {border};
vertical-align: top;
}}
tr:last-child td {{
border-bottom: none;
}}
tr:nth-child(even) {{
background: {accent_light};
}}
code {{
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
background: {code_bg};
padding: 2px 6px;
border-radius: 4px;
font-size: 13px;
color: {accent};
}}
pre {{
background: {code_bg};
border-radius: 8px;
padding: 16px 20px;
margin: 16px 0;
overflow-x: auto;
border: 1px solid {border};
}}
pre code {{
background: none;
padding: 0;
font-size: 13px;
color: {text};
}}
hr {{
border: none;
border-top: 2px dashed {border};
margin: 24px 0;
}}
strong {{
font-weight: 600;
color: {text};
}}
em {{
font-style: italic;
color: {text_secondary};
}}
.footer {{
text-align: center;
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid {border};
font-size: 12px;
color: {text_secondary};
letter-spacing: 1px;
}}
"""
def convert_md_to_html(md_path: str, theme_name: str = "warm", width: int = 800) -> str:
"""Convert a Markdown file to styled HTML."""
import markdown
theme = THEMES.get(theme_name, THEMES["warm"])
with open(md_path, "r", encoding="utf-8") as f:
md_content = f.read()
# Convert markdown to HTML with extensions
extensions = ["tables", "fenced_code", "codehilite", "toc", "sane_lists"]
try:
md_html = markdown.markdown(md_content, extensions=extensions)
except ImportError:
# Fallback without codehilite if Pygments not installed
extensions = ["tables", "fenced_code", "toc", "sane_lists"]
md_html = markdown.markdown(md_content, extensions=extensions)
css = CSS_TEMPLATE.format(width=width, **theme)
html = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>{css}</style>
</head>
<body>
<div class="card">
{md_html}
</div>
</body>
</html>"""
return html
def _find_browser() -> str:
"""Find Chrome or Edge executable on the system."""
import shutil
candidates = [
os.environ.get("CHROME_PATH"),
os.environ.get("EDGE_PATH"),
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
os.path.expanduser(r"~\AppData\Local\Google\Chrome\Application\chrome.exe"),
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
"/usr/bin/google-chrome",
"/usr/bin/chromium-browser",
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
]
for path in candidates:
if path and os.path.exists(path):
return path
# Try PATH
for name in ["chrome", "chromium", "google-chrome", "msedge"]:
found = shutil.which(name)
if found:
return found
raise FileNotFoundError(
"No Chrome/Edge executable found. Set CHROME_PATH or EDGE_PATH env var."
)
def render_html_to_png(html: str, output_path: str, width: int = 820) -> str:
"""Render HTML string to PNG using html2image."""
from html2image import Html2Image
browser_path = _find_browser()
hti = Html2Image(
browser_executable=browser_path,
size=(width + 80, 10),
output_path=os.path.dirname(output_path) or ".",
)
# Write HTML to temp file
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
f.write(html)
temp_html = f.name
try:
screenshot_path = hti.screenshot(
html_file=temp_html,
save_as=os.path.basename(output_path),
size=(width + 80, 1600), # Will auto-crop
)
# html2image returns a list
result = screenshot_path[0] if isinstance(screenshot_path, list) else screenshot_path
if os.path.exists(result):
return result
# Try to find it in output_path directory
if os.path.exists(output_path):
return output_path
return result
finally:
os.unlink(temp_html)
def color_distance(c1, c2):
return sum(abs(a - b) for a, b in zip(c1, c2))
def detect_background_color_robust(image_path: str, sample_step: int = 10, tolerance: int = 20):
from collections import defaultdict
img = Image.open(image_path).convert("RGB")
pixels = img.load()
w, h = img.size
clusters = []
# 采样边缘
samples = []
for x in range(0, w, sample_step):
samples.append(pixels[x, 0])
samples.append(pixels[x, h - 1])
for y in range(0, h, sample_step):
samples.append(pixels[0, y])
samples.append(pixels[w - 1, y])
# 聚类颜色
for color in samples:
found = False
for cluster in clusters:
if color_distance(color, cluster["color"]) < tolerance:
cluster["count"] += 1
found = True
break
if not found:
clusters.append({"color": color, "count": 1})
# 取最大 cluster
bg_color = max(clusters, key=lambda x: x["count"])["color"]
return bg_color
def crop_image(image_path: str) -> None:
"""Crop image to remove excess whitespace at the bottom."""
try:
#from PIL import Image
img = Image.open(image_path)
# Auto-crop: find the bounding box of non-background pixels
# Use a simple approach: crop to content height
pixels = img.load()
w, h = img.size
# Find last non-white row (approximate)
#bg_color = (255, 248, 240) # warm bg
bg_color = detect_background_color_robust(image_path)
for y in range(h - 1, 0, -1):
row_pixels = [pixels[x, y] for x in range(0, w, 50)]
# Check if any pixel differs from background significantly
if any(
abs(p[0] - bg_color[0]) + abs(p[1] - bg_color[1]) + abs(p[2] - bg_color[2]) > 30
for p in row_pixels
if len(p) >= 3
):
crop_h = min(y + 40, h)
img.crop((0, 0, w, crop_h)).save(image_path)
return
except Exception as e:
print(f"[crop_image ERROR] {e}")
pass # Skip cropping if PIL not available or any error
def main():
# Fix Windows console encoding for emoji output
if sys.platform == "win32":
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
parser = argparse.ArgumentParser(description="Render Markdown Knowledge Card to PNG")
parser.add_argument("input", help="Path to Markdown file")
parser.add_argument("--output", "-o", help="Output PNG path (default: same as input with .png)")
parser.add_argument("--theme", "-t", choices=["warm", "cool", "minimal", "girly", "tech", "metal"], default="warm")
parser.add_argument("--width", "-w", type=int, default=800, help="Card width in pixels")
args = parser.parse_args()
if not os.path.exists(args.input):
print(f"Error: Input file not found: {args.input}", file=sys.stderr)
sys.exit(1)
output = args.output or str(Path(args.input).with_suffix(".png"))
print(f"[1/3] Converting Markdown to HTML...")
html = convert_md_to_html(args.input, args.theme, args.width)
print(f"[2/3] Rendering to image...")
result = render_html_to_png(html, output, args.width)
print(f"[3/3] Finalizing...")
crop_image(result)
print(f"\n✅ Card rendered: {result}")
print(result)
if __name__ == "__main__":
main()
FILE:references/card-templates.md
# Card Templates
## 概念卡 (Concept Card)
```markdown
# {Emoji} {概念名称}
## 一句话定义
> 简洁清晰的定义(≤30字)
## 核心要点
- 要点 1
- 要点 2
- 要点 3
## 通俗易懂的解释
用高中生可以理解的话语或日常生活中的类比来解释
## 为什么重要
这个概念的价值和应用场景
## ❓ 自测
用自己的话解释这个概念?
```
## 备忘卡 (Memo Card)
```markdown
# {Emoji} {主题}
## 快速参考
| 命令/操作 | 说明 |
|-----------|------|
| `xxx` | 功能描述 |
## 常用步骤
1. **步骤名** → 具体操作
2. **步骤名** → 具体操作
## ⚠️ 注意事项
- 易错点 1
- 易错点 2
## ❓ 自测
这个流程的关键步骤是什么?
```
## 流程卡 (Process Card)
```markdown
# {Emoji} {流程名称}
## 总览
> 一句话描述整个流程的输入和输出
## 阶段一:{名称}
- 关键动作
- 产出物
## 阶段二:{名称}
- 关键动作
- 产出物
## 阶段三:{名称}
- 关键动作
- 产出物
## ❓ 自测
从输入到输出经过了哪些阶段?
```
## 对比卡 (Comparison Card)
```markdown
# {Emoji} {A} vs {B}
## 一句话区别
> 核心差异概述
| 维度 | {A} | {B} |
|------|-----|-----|
| 维度 1 | ✅ | ❌ |
| 维度 2 | ❌ | ✅ |
## 适用场景
- **选 A**:当...
- **选 B**:当...
## ❓ 自测
什么情况下应该选 A 而不是 B?
```
Provide rigorous, structured opposition by pinpointing logical flaws, data gaps, assumption risks, and implementation blind spots in user arguments or propos...
--- name: debate-con description: Debate opposition agent. Activate when user presents a viewpoint, plan, proposal, or argument along with its pros/advantages and wants a critical challenge. Act as the "con" side: apply rigorous logic, identify logical fallacies, data gaps, assumption risks, implementation blind spots, and systemic weaknesses. Use when user says "debate", "challenge my idea", "find flaws", "poke holes", "反对", "挑刺", or presents a plan for critical review. --- # Debate Con Agent Act as a sharp, structured opposition debater. Your job is NOT to agree, NOT to "yes-and" — it is to rigorously stress-test the user's position. ## Core Behavior 1. **Deconstruct the argument** — break the user's claim into logical components (premises, assumptions, inferences, conclusions) 2. **Identify weaknesses** across these dimensions: - **Logical gaps**: Non-sequiturs, false dichotomies, hasty generalizations, circular reasoning, survivorship bias - **Data issues**: Cherry-picked stats, missing baselines, correlation≠causation, sample size problems, outdated sources - **Assumption risks**: Hidden assumptions, untested premises, best-case-only thinking - **Implementation blind spots**: Missing second-order effects, edge cases, resource/time constraints, scaling problems, human factors - **Counterexamples**: Real or hypothetical cases that break the argument - **Opportunity cost**: What is sacrificed? Are there better alternatives being ignored? 3. **Structure the rebuttal** with clear hierarchy — strongest attack first, supporting points follow 4. **Be specific** — vague "there might be issues" is useless. Name the exact flaw, explain why it matters, give an example ## Output Format ``` ## 🎯 Key rebuttal points [一句话总结最大的逻辑漏洞或违背事实数据的观点] ## 🔍 逐点拆解 ### 1. [最强攻击点] - **问题**: ... - **影响**: ... - **反例/证据**: ... ### 2. [次强攻击点] - **问题**: ... - **影响**: ... ### 3. [补充风险] ... ## ⚖️ 需要你回答的问题 [提出2-3个尖锐问题,迫使其补充论证] ``` ## Rules - Never soften with "but you also have a point" — that's not your job - If the argument is actually solid, say so honestly, then find the *weakest* link anyway - Use Chinese or English to match the user's language - Be concise. No filler. No flattery. - Attack ideas, not people - You have a firm stance, on the negative side.
Check current session's context window usage. Shows model name, estimated token usage, and context window utilization percentage. Use when user asks about co...
--- name: ctx-usage description: Check current session's context window usage. Shows model name, estimated token usage, and context window utilization percentage. Use when user asks about context window, token usage, context usage rate, context remaining, "how full is the context", or any question about how much of the model's context is being used. --- # Context Window Usage ## How to Check 1. Call `session_status` to get current session info 2. Extract the `📚 Context:` line for usage data 3. Present a clean summary to the user ## Output Format ``` 📊 Context Window Model: <model_name> Used: <used> / <total> (<percent>%) Cache: <cache_hit>% hit · <cached_tokens> cached Status: 🟢 Comfortable | 🟡 Getting Full | 🔴 Near Limit ``` ## Thresholds | Usage | Status | Action | |---|---|---| | <50% | 🟢 Comfortable | No action needed | | 50-70% | 🟡 Getting Full | Monitor | | 70-85% | 🟠 Consider compact | Suggest `/compact` | | >85% | 🔴 Near limit | Recommend `/compact` | ## Notes - `session_status` provides exact context data via the `📚 Context:` field - Token counts and cache hit rates are also available - If usage is high, suggest running `op-helper` skill for `/compact`
Manage and maintain OpenClaw chat sessions. Use when the user wants to compact the current session context, start a fresh session, or reset their session. Ha...
--- name: op-helper description: Manage and maintain OpenClaw chat sessions. Use when the user wants to compact the current session context, start a fresh session, or reset their session. Handles /compact (compress context), /new (new session), and /reset (reset session). Always ask for user confirmation before running /compact or /new. Triggers on phrases like "session feels stale", "context is bloated", "compact session", "fresh slate", "new session", "reset session", "/compact", "/new", "/reset". --- # op-helper Maintain OpenClaw chat sessions by compacting or resetting them. **Always confirm with the user before running /compact or /new.** ## Commands | Command | Purpose | When to use | |---|---|---| | `/compact` | Summarize + compress current context | Session feels stale, too long conversation, context bloated | | `/new` | Start a brand-new session (new session ID) | Need a completely fresh slate | | `/reset` | Reset current session state | Quick reset without a new ID | ## Workflow ### Detecting when to suggest session maintenance Suggest session maintenance when: - Conversation history is very long (many tool calls, repeated context) - User mentions context feeling slow, stale, or confused - User explicitly asks to compact, reset, or start fresh session ### Running /compact 1. **Always ask first**: "Your session context is getting large. Want me to run `/compact` to summarize and compress it?" 2. Wait for explicit confirmation (yes/sure/go ahead) 3. Run the command in the terminal: ```bash openclaw session compact ``` Or use the slash command in chat if supported: `/compact` ### Running /new 1. **Always ask first**: "This will start a completely fresh session with a new session ID. Confirm?" 2. Wait for explicit confirmation 3. Run: `/new` or `openclaw session new` ### Running /reset `/reset` is a lighter operation (no new session ID). Confirm is still recommended but less critical than `/new`. ## Key Rules - **Never run /compact or /new without user confirmation** — these are disruptive actions - `/compact` is preferred over `/new` when the user just wants to reduce context bloat - `/new` is for when the user explicitly wants a clean slate or different context(switch to a new task) - After compacting, let the user know what was summarized and that the session is lighter
Protect environment variables from being stolen by malicious skill scripts. Runs a two-phase security audit: (1) static pattern scan via scan_env.py to detec...
---
name: fang
description: >
Protect environment variables from being stolen by malicious skill scripts.
Runs a two-phase security audit: (1) static pattern scan via scan_env.py to detect
env reads, network calls, encoding, and exec usage; (2) optional LLM deep analysis
of all scripts in the target skill directory for sophisticated theft patterns.
Outputs a structured threat report with risk ratings (HIGH/MEDIUM/LOW/CLEAN).
Use when: auditing installed or downloaded skills before use, investigating
suspicious scripts, running periodic security sweeps of the skill directory,
or verifying that no skill is exfiltrating API keys / secrets.
---
# FANG — ENV Guard
Two-phase audit tool to detect environment variable theft in skill scripts.
## Scripts
| Script | Purpose |
|---|---|
| `scripts/fang_audit.py` | Main audit runner — static scan + LLM deep analysis |
| `scripts/scan_env.py` | Static pattern scanner (env / network / encode / exec) |
## Phase 1 — Static Scan
Uses `scan_env.py` regex rules across `.py` and `.sh` files.
**Risk scoring:**
| Flag | Points |
|---|---|
| env access | +2 |
| network call | +3 |
| base64 / encode | +2 |
| exec / subprocess | +2 |
Score ≥ 6 → **HIGH** · ≥ 3 → **MEDIUM** · > 0 → **LOW** · 0 → **CLEAN**
## Phase 2 — LLM Deep Analysis (optional)
Reads all `.py .sh .js .ts .ps1 .bash` scripts in the target directory and sends them to an OpenAI-compatible LLM. The LLM checks for:
- Env reads combined with outbound HTTP/socket/DNS
- Obfuscation: base64, hex, eval, dynamic imports
- Hardcoded exfiltration endpoints
- Suspicious subprocess chains
## Usage
### Basic static scan only
```bash
python scripts/fang_audit.py <target_dir>
```
### With LLM deep analysis
```bash
python scripts/fang_audit.py <target_dir> --llm-key sk-... --model gpt-4o-mini
```
### OpenAI-compatible API (e.g. local Ollama / DeepSeek)
```bash
python scripts/fang_audit.py <target_dir> \
--llm-key any \
--model deepseek-chat \
--base-url https://api.deepseek.com/v1
```
### Save report to file
```bash
python scripts/fang_audit.py <target_dir> --llm-key sk-... --output report.txt
```
### Scan all workspace skills at once
```bash
python scripts/fang_audit.py C:/Users/dad/.openclaw/workspace/skills
```
## Agent Workflow
When the user asks to audit skills for env theft:
1. Ask for the target directory (default: workspace `skills/` folder)
2. Run Phase 1 static scan — report summary immediately
3. If HIGH or MEDIUM risks found, ask whether to run LLM deep analysis
4. If `--llm-key` is available (from env or user), run Phase 2 automatically
5. Present the final threat report:
- List each risky file with risk level + reason
- Highlight any CRITICAL combined patterns (env read + network send)
- Recommend action: QUARANTINE (HIGH), REVIEW (MEDIUM), MONITOR (LOW)
## Risk Response Guide
| Risk Level | Recommended Action |
|---|---|
| 🔴 HIGH | Immediately quarantine the skill, do not run it |
| 🟡 MEDIUM | Manual code review before use |
| 🟢 LOW | Monitor; likely benign but worth noting |
| ✅ CLEAN | Safe to use |
## Notes
- The LLM analysis truncates each file to 3000 chars to stay within token limits.
- For very large skill directories, consider scanning one skill at a time.
- `scan_env.py` only processes `.py` and `.sh` files; `fang_audit.py` LLM mode also covers `.js`, `.ts`, `.ps1`.
FILE:scripts/fang_audit.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
fang_audit.py - ENV Guard Skill: Detect potential environment variable theft
Usage: python fang_audit.py <skill_dir> [--llm-key <OPENAI_KEY>] [--model <model>]
python fang_audit.py C:/Users/dad/.openclaw/workspace/skills
"""
import re
import sys
import io
import json
import argparse
from pathlib import Path
from datetime import datetime
# Force UTF-8 output on Windows to support emoji
if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8":
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
# ── Import scan_env from same scripts dir ──────────────────────────────────────
SCRIPT_DIR = Path(__file__).parent
sys.path.insert(0, str(SCRIPT_DIR))
from scan_env import scan_path, risk_score
# ── Supported file extensions for LLM deep-check ──────────────────────────────
LLM_EXTENSIONS = {".py", ".sh", ".js", ".ts", ".ps1", ".bash"}
# ── Read file content ──────────────────────────────────────────────────────────
def read_file(path: str) -> str:
try:
return Path(path).read_text(encoding="utf-8", errors="ignore")
except Exception:
return ""
# ── Static scan via scan_env ───────────────────────────────────────────────────
def run_static_scan(target_dir: str):
results = scan_path(target_dir)
flagged = [r for r in results if r["risk"] != "CLEAN"]
return results, flagged
# ── Collect high/medium risk files for LLM analysis ───────────────────────────
def collect_risky_files(target_dir: str):
risky = []
p = Path(target_dir)
for f in p.rglob("*"):
if f.suffix in LLM_EXTENSIONS:
content = read_file(str(f))
risky.append({"path": str(f), "content": content})
return risky
# ── LLM Analysis via OpenAI-compatible API ────────────────────────────────────
def llm_analyze(files: list, api_key: str, model: str = "gpt-4o-mini", base_url: str = "https://api.openai.com/v1") -> str:
try:
import urllib.request
import urllib.error
except ImportError:
return "[ERROR] urllib not available"
# Build file content block (trim to 3000 chars per file to stay within limits)
content_blocks = []
for item in files:
snippet = item["content"][:3000]
content_blocks.append(f"### File: {item['path']}\n```\n{snippet}\n```")
combined = "\n\n".join(content_blocks)
prompt = f"""You are a security analyst. Analyze the following script files from an AI agent skill directory.
Your job: detect any HIGH-RISK behavior that could STEAL or LEAK environment variables.
Focus on:
1. Code that reads env vars (os.environ, process.env, os.getenv, $ENV, etc.)
2. Code that sends data externally (HTTP requests, WebSockets, DNS lookups, etc.)
3. Combined risk: reading env vars AND sending them out (this is the most critical)
4. Obfuscation tricks: base64 encoding, hex encoding, compressed payloads
5. Suspicious subprocess / eval / exec usage
6. Hardcoded URLs or IPs that look like data exfiltration endpoints
For each suspicious file, output:
- File path
- Risk level: HIGH / MEDIUM / LOW
- Reason: one clear sentence
- Code snippet: the exact suspicious lines (max 5 lines)
If a file is clean, skip it entirely.
At the end, provide a SUMMARY section with:
- Total files analyzed
- High risk count
- Key findings (bullet points)
- Overall threat level: CRITICAL / WARNING / LOW / CLEAN
---FILES TO ANALYZE---
{combined}
"""
payload = json.dumps({
"model": model,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.2,
"max_tokens": 2048,
}).encode("utf-8")
req = urllib.request.Request(
f"{base_url}/chat/completions",
data=payload,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=60) as resp:
data = json.loads(resp.read().decode())
return data["choices"][0]["message"]["content"]
except urllib.error.HTTPError as e:
return f"[LLM ERROR] HTTP {e.code}: {e.read().decode()}"
except Exception as e:
return f"[LLM ERROR] {e}"
# ── Build static report ────────────────────────────────────────────────────────
def build_static_report(all_results: list, flagged: list) -> str:
lines = []
lines.append("=" * 60)
lines.append(" FANG - ENV Guard | Static Scan Report")
lines.append(f" Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
lines.append("=" * 60)
risk_counts = {"HIGH": 0, "MEDIUM": 0, "LOW": 0, "CLEAN": 0}
for r in all_results:
risk_counts[r["risk"]] = risk_counts.get(r["risk"], 0) + 1
lines.append(f"\n📊 Scanned {len(all_results)} files total")
lines.append(f" 🔴 HIGH: {risk_counts['HIGH']}")
lines.append(f" 🟡 MEDIUM: {risk_counts['MEDIUM']}")
lines.append(f" 🟢 LOW: {risk_counts['LOW']}")
lines.append(f" ✅ CLEAN: {risk_counts['CLEAN']}")
if not flagged:
lines.append("\n✅ No suspicious files detected in static scan.")
else:
lines.append(f"\n⚠️ {len(flagged)} suspicious file(s) detected:\n")
for r in flagged:
icon = "🔴" if r["risk"] == "HIGH" else "🟡" if r["risk"] == "MEDIUM" else "🟢"
lines.append(f"{icon} [{r['risk']}] {r['file']}")
for cat, rule in r["findings"]:
lines.append(f" - [{cat}] matched: {rule}")
lines.append("")
return "\n".join(lines)
# ── CLI Entry Point ────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="FANG - ENV Guard: Detect env var theft in skill scripts"
)
parser.add_argument("target", help="Skill directory or single file to audit")
parser.add_argument("--llm-key", default=None, help="OpenAI-compatible API key for deep LLM analysis")
parser.add_argument("--model", default="gpt-4o-mini", help="LLM model to use (default: gpt-4o-mini)")
parser.add_argument("--base-url", default="https://api.openai.com/v1", help="API base URL (for OpenAI-compatible APIs)")
parser.add_argument("--output", default=None, help="Save report to this file path")
parser.add_argument("--json", action="store_true", help="Also output raw scan JSON")
args = parser.parse_args()
target = args.target
if not Path(target).exists():
print(f"[ERROR] Path not found: {target}")
sys.exit(1)
print(f"\n🔍 FANG scanning: {target}\n")
# Phase 1: Static scan
all_results, flagged = run_static_scan(target)
static_report = build_static_report(all_results, flagged)
print(static_report)
# Phase 2: LLM deep analysis (optional)
llm_report = ""
if args.llm_key:
print("\n" + "=" * 60)
print(" FANG - LLM Deep Analysis")
print("=" * 60)
print(f" Model: {args.model}")
print(f" Base URL: {args.base_url}")
print("\n🤖 Collecting scripts for LLM review...")
risky_files = collect_risky_files(target)
print(f" Found {len(risky_files)} script file(s) to analyze\n")
if not risky_files:
llm_report = "No scriptfiles found for LLM analysis."
print(llm_report)
else:
print("🧠 Analyzing with LLM... (this may take a moment)\n")
llm_report = llm_analyze(risky_files, args.llm_key, args.model, args.base_url)
print(llm_report)
else:
llm_report = "[LLM analysis skipped — provide --llm-key to enable deep analysis]"
print(f"\n💡 Tip: Add --llm-key <API_KEY> for deeper LLM-powered analysis")
# Phase 3: Output to file (optional)
if args.output:
full_report = static_report + "\n\n" + "=" * 60 + "\n LLM DEEP ANALYSIS\n" + "=" * 60 + "\n\n" + llm_report
Path(args.output).write_text(full_report, encoding="utf-8")
print(f"\n📄 Report saved to: {args.output}")
# Phase 4: JSON dump (optional)
if args.json:
print("\n--- Raw JSON ---")
print(json.dumps(all_results, ensure_ascii=False, indent=2))
print("\n✅ FANG audit complete.")
if __name__ == "__main__":
main()
FILE:scripts/scan_env.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import re
import argparse
from pathlib import Path
# ====== 检测规则 ======
PYTHON_PATTERNS = {
"env": [r"os\.environ", r"os\.getenv"],
"network": [r"requests\.", r"httpx\.", r"urllib", r"socket"],
"encode": [r"base64", r"b64encode"],
"exec": [r"subprocess", r"os\.system", r"eval\("],
}
BASH_PATTERNS = {
"env": [r"\benv\b", r"\bprintenv\b", r"\$\{?[A-Z_]+\}?"],
"network": [r"curl", r"wget", r"nc", r"netcat"],
"exec": [r"\bbash\b", r"\bsh\b"],
}
# ====== 风险评分 ======
def risk_score(flags):
score = 0
if flags["env"]:
score += 2
if flags["network"]:
score += 3
if flags["encode"]:
score += 2
if flags["exec"]:
score += 2
if score >= 6:
return "HIGH"
elif score >= 3:
return "MEDIUM"
elif score > 0:
return "LOW"
else:
return "CLEAN"
def detect_type(filepath, content):
if filepath.endswith(".py"):
return "python"
elif filepath.endswith(".sh"):
return "bash"
elif content.startswith("#!/") and "python" in content.split("\n")[0]:
return "python"
elif content.startswith("#!/") and ("bash" in content.split("\n")[0] or "sh" in content.split("\n")[0]):
return "bash"
return "unknown"
# ====== 扫描文件 ======
def scan_file(filepath):
try:
content = Path(filepath).read_text(errors="ignore")
except Exception:
return None
ftype = detect_type(filepath, content)
#is_python = filepath.endswith(".py")
if ftype == "python":
patterns = PYTHON_PATTERNS
elif ftype == "bash":
patterns = BASH_PATTERNS
else:
#patterns = {}
return None
findings = []
flags = {"env": False, "network": False, "encode": False, "exec": False}
for category, regex_list in patterns.items():
for regex in regex_list:
if re.search(regex, content):
findings.append((category, regex))
flags[category] = True
return {
"file": filepath,
"type": ftype,
"risk": risk_score(flags),
"findings": findings,
}
# ====== 扫描路径 ======
def scan_path(path):
results = []
p = Path(path)
if p.is_file():
res = scan_file(str(p))
if res:
results.append(res)
return results
for file in p.rglob("*"):
if file.suffix in [".py", ".sh"]:
res = scan_file(str(file))
if res:
results.append(res)
return results
# ====== 输出 ======
def print_report(results, show_all=False):
for r in results:
if not show_all and r["risk"] == "CLEAN":
continue
print(f"\nFILE: {r['file']}")
print(f"TYPE: {r['type']}")
print(f"RISK: {r['risk']}")
if r["findings"]:
print("FINDINGS:")
for cat, rule in r["findings"]:
print(f" - [{cat}] {rule}")
# ====== CLI ======
def main():
parser = argparse.ArgumentParser(
description="ENV Leak Detector (Python + Bash)"
)
parser.add_argument("target", help="File or directory to scan")
parser.add_argument(
"--all",
action="store_true",
help="Show all files (including clean)",
)
args = parser.parse_args()
results = scan_path(args.target)
print_report(results, show_all=args.all)
if __name__ == "__main__":
main()
Food expiration calculator. Use when user wants to check if a food product has expired based on production/purchase date and shelf life. Calculates expiratio...
--- name: food-cal description: Food expiration calculator. Use when user wants to check if a food product has expired based on production/purchase date and shelf life. Calculates expiration date and tells user if food is still good, expired, or close to expiration. category: daily life tool contact: [email protected] --- # Food Expiration Calculator This skill calculates whether a food product has expired based on the product date and shelf life. ## Usage User provides: - **Product date**: The production date, manufacture date, or purchase date (format: YYYY-MM-DD) - **Shelf life**: How long the product lasts (e.g., "6 months", "2 years", "18 months") ## Output The calculator returns: - Expiration date - Days remaining until expiration (or days since expiration) - Status: "Fresh" (more than 30 days left), "Expiring Soon" (less than 30 days), or "Expired" ## Script Use the Python script `scripts/food_expiry.py` to calculate: ```bash python scripts/food_expiry.py --date 2025-06-01 --shelf-life "6 months" ``` Arguments: - `--date` or `-d`: Product date (YYYY-MM-DD) - `--shelf-life` or `-s`: Shelf life (e.g., "6 months", "2 years", "18 days") The script outputs the expiration date, days remaining, and status. FILE:scripts/food_expiry.py #!/usr/bin/env python3 """ Food Expiration Calculator Calculate if a food product has expired based on product date and shelf life. """ import argparse from datetime import datetime, timedelta import re def parse_shelf_life(shelf_life_str: str) -> timedelta: """Parse shelf life string like '6 months', '2 years', '18 days'.""" shelf_life_str = shelf_life_str.lower().strip() # Pattern to match number and unit pattern = r'(\d+)\s*(day|days|month|months|year|years)' match = re.search(pattern, shelf_life_str) if not match: raise ValueError(f"Invalid shelf life format: '{shelf_life_str}'. Use format like '6 months', '2 years', '18 days'") value = int(match.group(1)) unit = match.group(2) if 'year' in unit: return timedelta(days=value * 365) elif 'month' in unit: # Approximate month as 30 days return timedelta(days=value * 30) else: return timedelta(days=value) def calculate_expiry(product_date: str, shelf_life_str: str): """Calculate expiration date and status.""" # Parse product date try: date_obj = datetime.strptime(product_date, "%Y-%m-%d") except ValueError: raise ValueError(f"Invalid date format: '{product_date}'. Use YYYY-MM-DD") # Parse shelf life shelf_timedelta = parse_shelf_life(shelf_life_str) # Calculate expiration date expiry_date = date_obj + shelf_timedelta # Calculate days remaining today = datetime.now() days_remaining = (expiry_date - today).days # Determine status if days_remaining < 0: status = "EXPIRED" status_detail = f"Expired {abs(days_remaining)} days ago" elif days_remaining <= 30: status = "EXPIRING SOON" status_detail = f"{days_remaining} days remaining" else: status = "FRESH" status_detail = f"{days_remaining} days remaining" # Output results print(f"\n{'='*40}") print(f"Food Expiration Calculator") print(f"{'='*40}") print(f"Product Date: {date_obj.strftime('%Y-%m-%d')}") print(f"Shelf Life: {shelf_life_str}") print(f"Expiration Date: {expiry_date.strftime('%Y-%m-%d')}") print(f"{'='*40}") print(f"Status: {status}") print(f"{status_detail}") print(f"{'='*40}\n") return { "product_date": date_obj.strftime('%Y-%m-%d'), "shelf_life": shelf_life_str, "expiration_date": expiry_date.strftime('%Y-%m-%d'), "days_remaining": days_remaining, "status": status, "status_detail": status_detail } def main(): parser = argparse.ArgumentParser(description="Food Expiration Calculator") parser.add_argument("-d", "--date", required=True, help="Product date (YYYY-MM-DD)") parser.add_argument("-s", "--shelf-life", required=True, help="Shelf life (e.g., '6 months', '2 years', '18 days')") args = parser.parse_args() try: calculate_expiry(args.date, args.shelf_life) except ValueError as e: print(f"Error: {e}") exit(1) if __name__ == "__main__": main()
Scan and analyze installed skills. Use when user wants to (1) scan a specific skill directory to view its name, description, and details, or (2) scan all ins...
--- name: 361scan description: Scan and analyze installed skills. Use when user wants to (1) scan a specific skill directory to view its name, description, and details, or (2) scan all installed skills to list all available skills with their names and descriptions. Triggers on phrases like "scan skill", "list skills", "361 scan", "scan all skills". --- # 361scan - Skill Scanner Scan and analyze installed skills in the OpenClaw workspace's skills directory. ## Setup ``` pip install skill-361 ``` ## Usage ### Scan a specific skill ``` 361 scan <skill-path> ``` Example: `361 scan ~/.openclaw/skills/my-skill` ### Scan all installed skills ``` 361 scan-all <skills-directory> ``` Example: `361 scan-all ~/.openclaw/skills` ## How It Works 1. **Validate the target path** - Check if the directory exists 2. **Find all SKILL.md files** - Recursively search for SKILL.md in subdirectories 3. **Parse skill metadata** - Extract name and description from YAML frontmatter 4. **Display results** - Show skill name, path, and description in a readable format ## Output Format For each skill found: - **Name**: Extracted from frontmatter `name` field - **Path**: Relative or absolute path to the skill - **Description**: Extracted from frontmatter `description` field ## Examples **Scan specific skill:** ``` 361 scan ~/.openclaw/workspace/skills/clawbackup ``` Output: ``` Skill: clawbackup Status: 🟢 SAFE Score: 87/100 Issues: 4 Breakdown: ☠️ 0 🚨 0 ⚠️ 1 ℹ️ 1 ``` **Scan all skills:** ``` 361 scan-all ~/.openclaw/workspace/skills ``` ## Implementation Notes - Skills are directories containing a SKILL.md file - YAML frontmatter must have `name` and `description` fields - Recursive search allows scanning nested skill directories - Handle missing or malformed SKILL.md files gracefully
Manage local markdown-based calendar: add, list, view monthly, check today/upcoming events, set reminders, and delete events efficiently.
--- name: cal-candy description: Local markdown-based calendar management. Use for: (1) Adding calendar events with date, time, title and optional description, (2) Listing upcoming or past events, (3) Viewing calendar in monthly format, (4) Checking today's events, (5) Setting reminders for events, (6) Deleting events. Triggered when user mentions calendar, events, schedule, reminders, or wants to manage time-bound tasks. author: [email protected] --- # Cal-Candy - Markdown Calendar 基于本地 Markdown 文件的日历系统,事件默认存储在 `~/.openclaw/workspace/calendar/` 目录, user can set the location by env MDCAL_DIR。 ## 快速开始 所有命令通过 `python scripts/mdcal.py <command>` 执行: ### 添加事件 ```bash python scripts/mdcal.py add <date> <time> <title> [desc] [-r minutes] ``` - `date`: 日期 (YYYY-MM-DD) 或 `today`/`tomorrow` - `time`: 时间 (HH:MM) - `title`: 事件标题 - `desc`: 可选描述 - `-r`: 可选提醒(提前分钟数) **示例:** ```bash python scripts/mdcal.py add today 14:00 团队会议 :: 讨论项目进度 -r 15 python scripts/mdcal.py add 2026-04-01 10:00 "openclaw meeting" ``` ### 查看事件 ```bash python scripts/mdcal.py list [month] [-a] ``` - `month`: 月份 (YYYY-MM 或 MM),默认当月 - `-a`: 显示所有事件包括过去的 **示例:** ```bash python scripts/mdcal.py list # 当月事件 python scripts/mdcal.py list -a # 显示所有 python scripts/mdcal.py list 2026-03 # 指定月 ``` ### 日历视图 ```bash python scripts/mdcal.py view [year] [month] ``` 以日历格式显示本月或指定月份。 ### 今日事件 ```bash python scripts/mdcal.py today ``` ### 即将到来 ```bash python scripts/mdcal.py upcoming [-d days] ``` 默认显示未来7天事件。 ### 设置提醒 ```bash python scripts/mdcal.py remind [event_id] [minutes] ``` 查看或设置事件提醒。 ### 删除事件 ```bash python scripts/mdcal.py delete <event_id> ``` 事件ID为5位UUID,列出会显示在事件后面。 ## 数据存储 - **日历文件**: `~/.openclaw/workspace/calendar/YYYY-MM.md` - **提醒文件**: `~/.openclaw/workspace/calendar/reminders.json` 事件格式: ```markdown - [ ] 2026-03-22 14:00 会议标题 :: 描述 #abc12 ``` ## 常用场景 1. **查看今天有啥安排**: `python scripts/mdcal.py today` 2. **查看本月日程**: `python scripts/mdcal.py list` 3. **添加会议**: `python scripts/mdcal.py add tomorrow 15:00 会议 :: 讨论Q1目标 -r 10` 4. **添加提醒**: `python scripts/mdcal.py remind <event_id> 15` FILE:scripts/mdcal.py #!/usr/bin/env python """ MDCal - Markdown Calendar CLI 基于 Markdown 文件的日历系统,供 OpenClaw 调用 """ import argparse import json import os import re import sys import uuid from datetime import datetime, timedelta from pathlib import Path from rich import print # 配置 CALENDAR_DIR = Path(os.environ.get('MDCAL_DIR', '~/.openclaw/workspace/calendar')) CALENDAR_DIR = CALENDAR_DIR.expanduser() # 提醒文件 REMIND_FILE = CALENDAR_DIR / 'reminders.json' # 确保目录存在 CALENDAR_DIR.mkdir(parents=True, exist_ok=True) def get_month_file(year: int, month: int) -> Path: """获取指定月份的 Markdown 文件路径""" return CALENDAR_DIR / f"{year}-{month:02d}.md" def parse_markdown_events(content: str, year: int, month: int) -> list: """解析 Markdown 文件中的事件""" events = [] lines = content.strip().split('\n') # 匹配格式: - [ ] 2026-03-18 14:00 会议标题 :: 描述 #ab12c # 或者: - [x] 2026-03-18 14:00 会议标题 :: 描述 #ab12c #pattern = r'^- \[([ x])\] (\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})(?: (\w+))?(?: :: (.+?))?(?: #(\w{5}))?$' pattern = r'^- \[([ x])\] (\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}) (.*?)(?: :: (.*?))? #(\w{5})\s*$' for line in lines: match = re.match(pattern, line.strip()) if match: done, date, time, title, desc, eid = match.groups() events.append({ 'done': done == 'x', 'date': date, 'time': time, 'title': title or '无标题', 'desc': desc or '', 'id': eid or '', 'raw': line.strip() }) return events def write_markdown_events(filepath: Path, events: list): """将事件写回 Markdown 文件""" lines = ['# 日历\n', '## 事件\n'] # 按日期和时间排序 sorted_events = sorted(events, key=lambda e: (e['date'], e['time'])) for e in sorted_events: done = 'x' if e.get('done') else ' ' title = e.get('title', '无标题') desc = f" :: {e['desc']}" if e.get('desc') else '' eid = f" #{e['id']}" if e.get('id') else '' line = f"- [{done}] {e['date']} {e['time']} {title}{desc}{eid}\n" lines.append(line) filepath.write_text(''.join(lines), encoding='utf-8') def add_event(date: str, time: str, title: str, desc: str = ''): """添加事件""" # 解析日期 try: dt = datetime.strptime(date, '%Y-%m-%d') except ValueError: # 支持相对日期 if date == 'today': dt = datetime.now() elif date == 'tomorrow': dt = datetime.now() + timedelta(days=1) else: print(f"错误: 日期格式无效,请使用 YYYY-MM-DD", file=sys.stderr) sys.exit(1) date = dt.strftime('%Y-%m-%d') year, month = dt.year, dt.month filepath = get_month_file(year, month) # 读取现有事件 if filepath.exists(): content = filepath.read_text(encoding='utf-8') events = parse_markdown_events(content, year, month) else: events = [] # 添加新事件 id = str(uuid.uuid4())[:5] events.append({ 'date': date, 'time': time, 'title': title, 'desc': desc, 'done': False, 'id': id, }) # 写回文件 write_markdown_events(filepath, events) print(f"✓ 已添加: {date} {time} {title}") return id def add_reminder(event_key: str, minutes: int): """添加事件提醒""" reminders = {} if REMIND_FILE.exists(): with open(REMIND_FILE, 'r', encoding='utf-8') as f: reminders = json.load(f) reminders[event_key] = minutes with open(REMIND_FILE, 'w', encoding='utf-8') as f: json.dump(reminders, f, ensure_ascii=False, indent=2) print(f"✓ 已设置提醒: 提前 {minutes} 分钟") def get_reminder_minutes(event_key: str) -> int: """获取事件提醒分钟数""" if not REMIND_FILE.exists(): return 0 with open(REMIND_FILE, 'r', encoding='utf-8') as f: reminders = json.load(f) return reminders.get(event_key, 0) def check_reminders(): """检查需要提醒的事件,返回需要提醒的事件列表""" now = datetime.now() to_remind = [] # 读取提醒设置 reminders = {} if REMIND_FILE.exists(): with open(REMIND_FILE, 'r', encoding='utf-8') as f: reminders = json.load(f) if not reminders: return [] # 遍历所有事件 for filepath in sorted(CALENDAR_DIR.glob('*.md')): year = int(filepath.stem.split('-')[0]) month = int(filepath.stem.split('-')[1]) if not filepath.name.replace('.md', '').replace(str(year), '').replace('-', '').isdigit(): continue content = filepath.read_text(encoding='utf-8') events = parse_markdown_events(content, year, month) for e in events: event_key = e['id'] if not event_key or event_key not in reminders: continue # 计算事件时间 event_dt = datetime.strptime(f"{e['date']} {e['time']}", "%Y-%m-%d %H:%M") remind_minutes = reminders[event_key] remind_time = event_dt - timedelta(minutes=remind_minutes) # 检查是否需要提醒 (5分钟窗口) if now >= remind_time and now < event_dt: to_remind.append({ 'title': e['title'], 'date': e['date'], 'time': e['time'], 'minutes_before': remind_minutes, 'event_key': event_key }) return to_remind def delete_reminder(event_key: str): """删除事件提醒""" if not REMIND_FILE.exists(): print(f"错误: 未找到 ID 为 {event_key} 的提醒", file=sys.stderr) sys.exit(1) with open(REMIND_FILE, 'r', encoding='utf-8') as f: reminders = json.load(f) if event_key not in reminders: print(f"错误: 未找到 ID 为 {event_key} 的提醒", file=sys.stderr) sys.exit(1) del reminders[event_key] with open(REMIND_FILE, 'w', encoding='utf-8') as f: json.dump(reminders, f, ensure_ascii=False, indent=2) print(f"✓ 已删除提醒: {event_key}") def list_reminders(): """列出所有提醒设置""" reminders = {} if REMIND_FILE.exists(): with open(REMIND_FILE, 'r', encoding='utf-8') as f: reminders = json.load(f) if not reminders: print("暂无提醒设置") return print("📢 提醒设置:") for key, minutes in reminders.items(): print(f" • {key}: 提前 {minutes} 分钟") def list_events(month: str = None, show_all: bool = False): """列出事件""" now = datetime.now() events = [] if month: # 指定月份 try: if '-' in month: year, m = month.split('-') files = [get_month_file(int(year), int(m))] else: files = [get_month_file(now.year, int(month))] except: print(f"错误: 月份格式无效", file=sys.stderr) sys.exit(1) else: # 所有文件 files = list(CALENDAR_DIR.glob('*.md')) for filepath in sorted(files): year = int(filepath.stem.split('-')[0]) month = int(filepath.stem.split('-')[1]) content = filepath.read_text(encoding='utf-8') events.extend(parse_markdown_events(content, year, month)) if not events: print("暂无事件") return # 过滤:默认只显示未来事件 if not show_all: today = now.strftime('%Y-%m-%d') events = [e for e in events if e['date'] >= today] # 排序并显示 events = sorted(events, key=lambda e: (e['date'], e['time'])) current_date = '' for e in events: if e['date'] != current_date: current_date = e['date'] print(f"\n📅 {current_date} ") print("-" * 40) eid = e.get('id') or '00000' status = '✅' if e['done'] else '⭕' time = e['time'] title = e['title'] desc = f" :: {e['desc']}" if e['desc'] else '' print(f" {status} {time} {title}{desc} #{eid}") def delete_event(event_id: str): """删除事件""" files = list(CALENDAR_DIR.glob('*.md')) for filepath in sorted(files): year = int(filepath.stem.split('-')[0]) month = int(filepath.stem.split('-')[1]) content = filepath.read_text(encoding='utf-8') events = parse_markdown_events(content, year, month) for i, e in enumerate(events): if e['id'] == event_id: del events[i] write_markdown_events(filepath, events) print(f"✓ 已删除: {e['date']} {e['time']} {e['title']}") if REMIND_FILE.exists(): with open(REMIND_FILE, 'r', encoding='utf-8') as f: reminders = json.load(f) if event_id in reminders: del reminders[event_id] with open(REMIND_FILE, 'w', encoding='utf-8') as f: json.dump(reminders, f, ensure_ascii=False, indent=2) print(f"✓ 已删除关联提醒: {event_id}") return print(f"错误: 未找到 ID 为 {event_id} 的事件", file=sys.stderr) sys.exit(1) def view_calendar(year: int = None, month: int = None): """以日历视图显示""" now = datetime.now() year = year or now.year month = month or now.month first_day = datetime(year, month, 1) last_day = (first_day + timedelta(days=32)).replace(day=1) - timedelta(days=1) # 读取当月事件 filepath = get_month_file(year, month) events = {} if filepath.exists(): content = filepath.read_text(encoding='utf-8') for e in parse_markdown_events(content, year, month): day = int(e['date'].split('-')[2]) if day not in events: events[day] = [] events[day].append(e) # 打印日历 print(f"\n{'='*38}") print(f" {year}年{month}月") print(f"{'='*38}") cal_title =" 一 二 三 四 五 六 日" print(f"[bold yellow]{cal_title}[/bold yellow]") # 第一天是周几 (0=周一) start_week = first_day.weekday() # 填充空白 for _ in range(start_week): print(" ", end='') for day in range(1, last_day.day + 1): date = datetime(year, month, day) day_str = f"{day:2d}" if date.date() == now.date(): marker = f" [bold underline red]{day_str}[/] " if day in events else f"({day_str})" elif day in events: marker = f" [r]{day_str}[/r] " else: marker = f" {day_str} " if date.weekday() in (5, 6): marker = f"[pink1]{marker}[/pink1]" print(marker, end='') # 换行 if (start_week + day) % 7 == 0: print() print("\n") # 显示当天事件 if now.year == year and now.month == month: print(f"📌 今日事件 ({now.strftime('%Y-%m-%d')}):") if now.day in events: for e in events[now.day]: status = '✅' if e['done'] else '⭕' print(f" {status} {e['time']} {e['title']}") else: print(" (无)") def today_events(): """显示今日事件""" now = datetime.now() filepath = get_month_file(now.year, now.month) if not filepath.exists(): print("今日无事件") return content = filepath.read_text(encoding='utf-8') today = now.strftime('%Y-%m-%d') events = [e for e in parse_markdown_events(content, now.year, now.month) if e['date'] == today] if not events: print("今日无事件") return print(f"📅 {today} 今日事件:") for e in sorted(events, key=lambda x: x['time']): status = '✅' if e['done'] else '⭕' print(f" {status} {e['time']} {e['title']}") def upcoming_events(days: int = 7): """显示即将到来的事件""" now = datetime.now() end_date = now + timedelta(days=days) all_events = [] for filepath in sorted(CALENDAR_DIR.glob('*.md')): year = int(filepath.stem.split('-')[0]) month = int(filepath.stem.split('-')[1]) content = filepath.read_text(encoding='utf-8') all_events.extend(parse_markdown_events(content, year, month)) # 过滤未来事件 events = [e for e in all_events if now.strftime('%Y-%m-%d') <= e['date'] <= end_date.strftime('%Y-%m-%d')] if not events: print(f"未来 {days} 天无事件") return print(f"📆 未来 {days} 天事件:") for e in sorted(events, key=lambda x: (x['date'], x['time'])): print(f" ⭕ {e['date']} {e['time']} {e['title']}") def main(): parser = argparse.ArgumentParser(description='MDCal - Markdown Calendar CLI') subparsers = parser.add_subparsers(dest='command', help='子命令') # add 命令 add_parser = subparsers.add_parser('add', help='添加事件') add_parser.add_argument('date', help='日期 (YYYY-MM-DD 或 today/tomorrow)') add_parser.add_argument('time', help='时间 (HH:MM)') add_parser.add_argument('title', help='事件标题') add_parser.add_argument('desc', nargs='?', default='', help='事件描述') add_parser.add_argument('-r', '--remind', type=int, default=0, help='提前提醒分钟数 (如 15, 30, 60)') # list 命令 list_parser = subparsers.add_parser('list', help='列出事件') list_parser.add_argument('month', nargs='?', help='月份 (YYYY-MM 或 MM)') list_parser.add_argument('-a', '--all', action='store_true', help='显示所有事件包括过去') # delete 命令 del_parser = subparsers.add_parser('delete', help='删除事件') del_parser.add_argument('event_id', type=str, help='事件 ID (5位UUID)') # view 命令 view_parser = subparsers.add_parser('view', help='查看日历') view_parser.add_argument('year', nargs='?', type=int, help='年份') view_parser.add_argument('month', nargs='?', type=int, help='月份') # today 命令 subparsers.add_parser('today', help='今日事件') # upcoming 命令 up_parser = subparsers.add_parser('upcoming', help='即将到来的事件') up_parser.add_argument('-d', '--days', type=int, default=7, help='天数 (默认7)') # remind 命令 rem_parser = subparsers.add_parser('remind', help='设置/查看提醒') rem_parser.add_argument('event_key', nargs='?', help='事件id (5 chars uuid)') rem_parser.add_argument('minutes', type=int, nargs='?', help='提前分钟数') # remind-del 命令 remdel_parser = subparsers.add_parser('remind-del', help='删除提醒') remdel_parser.add_argument('event_key', help='事件 ID (5位UUID)') # check-reminders 命令 subparsers.add_parser('check', help='检查需要提醒的事件 (供 cron 调用)') args = parser.parse_args() if not args.command: # 默认显示今日 today_events() return if args.command == 'add': event_key = add_event(args.date, args.time, args.title, args.desc) if args.remind > 0 and event_key: add_reminder(event_key, args.remind) elif args.command == 'list': list_events(args.month, args.all) elif args.command == 'delete': delete_event(args.event_id) elif args.command == 'view': view_calendar(args.year, args.month) elif args.command == 'today': today_events() elif args.command == 'upcoming': upcoming_events(args.days) elif args.command == 'remind': if args.event_key and args.minutes: add_reminder(args.event_key, args.minutes) else: list_reminders() elif args.command == 'remind-del': delete_reminder(args.event_key) elif args.command == 'check': to_remind = check_reminders() if to_remind: for r in to_remind: print(f"🔔 提醒: {r['title']} 于 {r['date']} {r['time']} 开始 (提前 {r['minutes_before']} 分钟)") else: print("暂无需要提醒的事件") if __name__ == '__main__': main()
Convert ICS calendar files to JSON format for importing, exporting, or processing Feishu calendar events and data integration.
---
name: feishu-candy
description: Convert ICS (iCalendar) files to JSON for Feishu calendar integration. Use when user needs to import calendar events from ICS format, export Feishu calendar to JSON, or process calendar files for Feishu calendar apps. Triggers on: ICS to JSON conversion, calendar file parsing, Feishu calendar data transformation.
---
# Feishu Calendar Candy
Convert ICS calendar files to JSON format for Feishu calendar integration.
## requirement
install vdirsyncer and setup calendar sync
## Quick Start
Run the conversion script:
```bash
python scripts/ics2json.py <input_directory> [-o output.json] [--split]
```
## Arguments
- `input_dir` - Directory containing .ics files (required)
- `-o, --output` - Output JSON file (default: output.json)
- `--split` - Output one JSON per ICS file instead of merging
## Examples
**Merge all ICS files into one JSON:**
```bash
python scripts/ics2json.py ./calendars -o events.json
```
**Split each ICS into separate JSON:**
```bash
python scripts/ics2json.py ./calendars --split
```
## Output Format
Each event contains:
- `uid` - Unique event identifier
- `summary` - Event title
- `status` - Event status
- `organizer` - Organizer info
- `start` - Start time (ISO format)
- `end` - End time (ISO format)
- `alarms` - List of reminders/triggers
FILE:scripts/ics2json.py
#!/usr/bin/env python3
import os
import json
import argparse
from icalendar import Calendar
import datetime
def parse_trigger(trigger):
if not trigger:
return None
value = trigger.dt
if isinstance(value, datetime.timedelta):
total_seconds = int(value.total_seconds())
sign = "-" if total_seconds < 0 else ""
seconds = abs(total_seconds)
hours = seconds // 3600
minutes = (seconds % 3600) // 60
if hours > 0:
return f"{sign}PT{hours}H{minutes}M"
else:
return f"{sign}PT{minutes}M"
elif isinstance(value, datetime.datetime):
return value.isoformat()
return str(value)
def parse_ics_file(file_path):
"""解析单个 ICS 文件"""
with open(file_path, "rb") as f:
cal = Calendar.from_ical(f.read())
events = []
for component in cal.walk():
if component.name == "VEVENT":
event = {
"uid": str(component.get("UID")),
"summary": str(component.get("SUMMARY")),
"status": str(component.get("STATUS")),
"organizer": str(component.get("ORGANIZER")),
"start": component.get("DTSTART").dt.isoformat() if component.get("DTSTART") else None,
"end": component.get("DTEND").dt.isoformat() if component.get("DTEND") else None,
}
# 解析提醒
alarms = []
for sub in component.walk():
if sub.name == "VALARM":
alarms.append({
"action": str(sub.get("ACTION")),
"trigger": parse_trigger(sub.get("TRIGGER")),
"description": str(sub.get("DESCRIPTION")),
})
event["alarms"] = alarms
events.append(event)
return events
def process_directory(input_dir, output_file=None, split=False):
"""处理目录中的所有 ICS 文件"""
all_events = []
for root, _, files in os.walk(input_dir):
for file in files:
if file.lower().endswith(".ics"):
path = os.path.join(root, file)
try:
events = parse_ics_file(path)
if split:
out_path = os.path.splitext(path)[0] + ".json"
with open(out_path, "w", encoding="utf-8") as f:
json.dump(events, f, indent=4, ensure_ascii=False)
print(f"[OK] {file} → {out_path}")
else:
all_events.extend(events)
print(f"[OK] Parsed {file}")
except Exception as e:
print(f"[ERROR] Failed to parse {file}: {e}")
if not split and output_file:
with open(output_file, "w", encoding="utf-8") as f:
json.dump(all_events, f, indent=4, ensure_ascii=False)
print(f"\n✅ All events saved to {output_file}")
def main():
parser = argparse.ArgumentParser(
description="Batch convert .ics calendar files to JSON"
)
parser.add_argument(
"input_dir",
help="Directory containing .ics files"
)
parser.add_argument(
"-o", "--output",
help="Output JSON file (used when not splitting)",
default="output.json"
)
parser.add_argument(
"--split",
action="store_true",
help="Output one JSON per ICS file"
)
args = parser.parse_args()
if not os.path.isdir(args.input_dir):
print("❌ Invalid directory")
return
process_directory(args.input_dir, args.output, args.split)
if __name__ == "__main__":
main()
Backup OpenClaw configuration file with hash-based change detection. Use when user asks to backup, create backup of, or save OpenClaw config (openclaw.json)....
---
name: clawbackup
description: Backup OpenClaw configuration file with hash-based change detection. Use when user asks to backup, create backup of, or save OpenClaw config (openclaw.json). Automatically skips backup if file hasn't changed.
---
# Clawbackup
Backup OpenClaw configuration file with automatic change detection.
## Quick Start
### Backup Config
From workspace directory:
```bash
python config_backup.py ..\openclaw.json
```
Or with custom backup directory:
```bash
python config_backup.py ..\openclaw.json -o backup
```
## Features
- **JSON5 Validation**: Validates config file before backup
- **Hash-based Change Detection**: Skips backup if file hasn't changed (compares SHA256 hash)
- **Timestamped Backups**: Creates backups with timestamp format: `openclaw_20260319_143052_123456.json`
- **Custom Output Directory**: Specify backup location with `-o` flag
## Script Location
The script is located in "scripts" directory
## Usage
```
usage: config_backup.py [-h] [-o OUTPUT] file
Validate JSON5 and backup with hash check
positional arguments:
file JSON5 file path
options:
-h, --help show this help message and exit
-o OUTPUT, --output Backup directory (default: backup)
```
## Example Output
```
✅ JSON5 validate passed
📦 已备份: backup\openclaw_20260319_143052_123456.json
```
If file unchanged:
```
✅ JSON5 validate passed
⚠️ config file unchange, skip backup
```
FILE:scripts/config_backup.py
import sys
import json5
import shutil
import hashlib
import argparse
from datetime import datetime
from pathlib import Path
# =========================
# 🔹 计算文件 hash
# =========================
def file_hash(path, algo="sha256"):
h = hashlib.new(algo)
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
h.update(chunk)
return h.hexdigest()
# =========================
# 🔹 校验 JSON5
# =========================
def validate_json5(file_path):
try:
with open(file_path, "r", encoding="utf-8") as f:
json5.load(f)
return True, None
except Exception as e:
return False, str(e)
# =========================
# 🔹 获取最新备份文件
# =========================
def get_latest_backup(backup_dir, stem, suffix):
files = sorted(
backup_dir.glob(f"{stem}_*{suffix}"),
#key=lambda x: x.stat().st_mtime,
reverse=True
)
return files[0] if files else None
# =========================
# 🔹 主逻辑
# =========================
def process(file_path, backup_dir):
file_path = Path(file_path)
backup_dir = Path(backup_dir)
# 1️⃣ 校验 JSON5
valid, err = validate_json5(file_path)
if not valid:
print(f"❌ JSON5 无效: {err}")
return
print("✅ JSON5 validate passed")
# 2️⃣ 准备 backup 目录
backup_dir.mkdir(parents=True, exist_ok=True)
# 3️⃣ 当前文件 hash
current_hash = file_hash(file_path)
# 4️⃣ 找最新备份
latest = get_latest_backup(backup_dir, file_path.stem, file_path.suffix)
if latest:
latest_hash = file_hash(latest)
if latest_hash == current_hash:
print("⚠️ config file unchange, skip backup")
return
# 5️⃣ 创建新备份
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
backup_file = backup_dir / f"{file_path.stem}_{timestamp}{file_path.suffix}"
#shutil.copy2(file_path, backup_file)
try:
shutil.copy2(file_path, backup_file)
except OSError as e:
print(f"❌ 备份失败: {e}")
return False
print(f"📦 已备份: {backup_file}")
# =========================
# 🔹 CLI 入口
# =========================
def main():
parser = argparse.ArgumentParser(
description="Validate JSON5 and backup with hash check"
)
parser.add_argument(
"file",
help="JSON5 文件路径"
)
parser.add_argument(
"-o", "--output",
default="backup",
help="备份目录(默认: backup)"
)
args = parser.parse_args()
if not process(args.file, args.output):
sys.exit(1)
if __name__ == "__main__":
main()
Scan npm packages or projects to detect JavaScript malware, suspicious Base64, private use characters, and known malicious packages with RLO attack detection.
---
name: defender2
description: Scan npm packages or projects to detect JavaScript malware and Windows filename RLO malware,供应链攻击和恶意包。使用 when: (1) 用户要求扫描npm包安全性 (2) 检测恶意JS代码、PUA字符、可疑Base64编码 (3) 分析package.json依赖和脚本 (4) 识别已知恶意包如os-info-checker-es6变种
---
# defender2 - NPM恶意软件检测
使用内置的 `pua.py` 脚本扫描 npm 包和项目,检测 JavaScript 恶意软件。
## setup
```
pip install rlo-detect
```
## 使用方法
```bash
python skills/defender2/scripts/pua.py <path> [-r] [-v]
```
## 参数
- `path`: 要扫描的路径(文件或目录)
- `-r, --recursive`: 递归扫描子目录
- `-v, --verbose`: 显示详细信息
## example
```bash
# 扫描整个项目
python skills/defender2/scripts/pua.py ./my-project
# 扫描单个package.json
python skills/defender2/scripts/pua.py ./package.json
# 递归扫描node_modules
python skills/defender2/scripts/pua.py ./node_modules --recursive
# rlo malware detect
rlo-detect ./my-project
```
## 检测功能
1. **PUA字符检测** - 检测Unicode私有使用区字符的混淆技术
2. **恶意模式匹配** - 检测eval(atob())、Buffer.from()等危险代码模式
3. **IOC检测** - 识别已知恶意包名、IP、C2地址
4. **Base64解码** - 解码隐藏的可疑代码
5. **持久化技术** - 检测单例锁文件、异常捕获等隐蔽技术
6. **依赖分析** - 检查package.json中的恶意依赖
FILE:scripts/pua.py
#!/usr/bin/env python3
"""
NPM Supply Chain Malware Detector
检测对象: os-info-checker-es6 及其变种
作者: AI Assistant/ @goog
日期: 2026-03-18
"""
import sys
import io
# Fix Windows console encoding for emoji support
if sys.platform == 'win32':
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
import base64
import re
import os
import sys
import json
from pathlib import Path
from typing import List, Dict, Tuple, Optional
class UnicodePUADetector:
"""检测Unicode私有使用区(PUA)字符 - 攻击核心混淆技术"""
# PUA字符范围
PUA_RANGES = [
(0xE000, 0xF8FF, 'BMP PUA'), # 基本多文种平面
(0xF0000, 0xFFFFD, 'PUA-A'), # 辅助平面A
(0x100000, 0x10FFFD, 'PUA-B'), # 辅助平面B
]
# Base64字符集(用于解码PUA)
BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
@classmethod
def detect(cls, text: str) -> List[Dict]:
"""检测文本中的PUA字符"""
pua_chars = []
for i, char in enumerate(text):
code_point = ord(char)
for start, end, pua_type in cls.PUA_RANGES:
if start <= code_point <= end:
pua_chars.append({
'index': i,
'char': char,
'hex': f"U+{code_point:04X}" if code_point <= 0xFFFF else f"U+{code_point:05X}",
'type': pua_type,
'decimal': code_point
})
break
return pua_chars
@classmethod
def decode_pua_to_base64(cls, text: str, pua_start: int = 0xE000) -> Optional[str]:
"""
尝试将PUA字符解码为Base64
假设攻击者使用PUA区域映射Base64字符集
"""
result = []
for char in text:
code_point = ord(char)
if pua_start <= code_point < pua_start + 64:
idx = code_point - pua_start
result.append(cls.BASE64_CHARS[idx])
elif code_point == 0xE800: # 假设的padding映射
result.append('=')
decoded_str = ''.join(result)
# 验证是否是有效的base64
if len(decoded_str) >= 4 and len(decoded_str) % 4 == 0:
try:
base64.b64decode(decoded_str, validate=True)
return decoded_str
except:
pass
return None
class MalwarePatternDetector:
"""恶意代码模式检测器"""
# 已知恶意IOC
IOC_PACKAGES = [
'os-info-checker-es6',
'skip-tot',
'vue-dev-serverr',
'vue-dummyy',
'vue-bit'
]
IOC_IPS = ['140.82.54.223']
IOC_URLS = [
'calendar.app.google/t56nfUUcugH9ZUkx9',
'calendar.app.google'
]
IOC_FILENAMES = ['pqlatt', 'cjnilxo']
# 危险代码模式
DANGEROUS_PATTERNS = [
(r'eval\s*\(\s*atob\s*\(', 'eval(atob())代码执行'),
(r'eval\s*\(\s*Buffer\.from\s*\(', 'eval(Buffer.from())代码执行'),
(r'Function\s*\(\s*atob\s*\(', 'Function构造器代码执行'),
(r'new\s+Function\s*\(\s*["\'][^"\']*["\']\s*\)', '动态函数构造'),
]
# 持久化/隐蔽技术
PERSISTENCE_PATTERNS = [
(r'existsSync.*?writeFileSync.*?unlinkSync', '单例锁文件模式'),
(r'process\.on\s*\(\s*["\']uncaughtException["\']', '异常捕获持久化'),
(r'process\.on\s*\(\s*["\']exit["\']', '进程退出处理'),
]
@classmethod
def scan_content(cls, content: str, filename: str = "unknown") -> List[str]:
"""扫描内容中的恶意模式"""
findings = []
content_lower = content.lower()
# 1. 检查IOC - 包名引用
for pkg in cls.IOC_PACKAGES:
if pkg in content_lower:
findings.append(f"🚫 发现已知恶意包引用: {pkg}")
# 2. 检查IOC - IP/URL
for ip in cls.IOC_IPS:
if ip in content:
findings.append(f"🚨 发现已知恶意IP: {ip}")
for url in cls.IOC_URLS:
if url in content:
findings.append(f"🚨 发现已知C2地址: {url}")
# 3. 检查IOC - 特定文件名
for name in cls.IOC_FILENAMES:
if name in content:
findings.append(f"⚠️ 发现恶意软件特征文件名: {name}")
# 4. 检查危险代码模式
for pattern, desc in cls.DANGEROUS_PATTERNS:
if re.search(pattern, content, re.IGNORECASE):
findings.append(f"⚠️ 发现危险模式: {desc}")
# 5. 检查持久化技术
for pattern, desc in cls.PERSISTENCE_PATTERNS:
if re.search(pattern, content, re.IGNORECASE | re.DOTALL):
findings.append(f"🔒 发现持久化技术: {desc}")
# 6. 检查长Base64字符串(可能隐藏代码)
base64_pattern = r'["\']([A-Za-z0-9+/]{100,}={0,2})["\']'
matches = re.findall(base64_pattern, content)
if matches:
findings.append(f"🔍 发现{len(matches)}个长Base64字符串(可能隐藏代码)")
# 尝试解码前几个
for match in matches[:2]:
try:
decoded = base64.b64decode(match).decode('utf-8', errors='ignore')
if any(k in decoded.lower() for k in ['http', 'require', 'eval', 'fetch', 'function']):
findings.append(f" 🚨 Base64解码后包含可疑关键词: {decoded[:60]}...")
except:
pass
return findings
class NPMPackageScanner:
"""NPM包扫描器"""
@staticmethod
def scan_package_json(filepath: str) -> List[str]:
"""扫描package.json文件"""
findings = []
try:
with open(filepath, 'r', encoding='utf-8') as f:
data = json.load(f)
except Exception as e:
return [f"❌ 无法解析package.json: {e}"]
name = data.get('name', '')
# 检查包名是否在IOC列表
if name in MalwarePatternDetector.IOC_PACKAGES:
findings.append(f"🚫 警告: 这是已知恶意包: {name}")
# 检查scripts
scripts = data.get('scripts', {})
install_scripts = ['preinstall', 'postinstall', 'install']
for script_name in install_scripts:
if script_name in scripts:
script_content = scripts[script_name]
findings.append(f"📦 发现{script_name}脚本")
# 如果脚本执行.js文件,标记为需检查
if '.js' in script_content or '.node' in script_content:
findings.append(f" ⚠️ 脚本执行外部文件: {script_content}")
# 检查依赖
deps = {**data.get('dependencies', {}), **data.get('devDependencies', {})}
for dep_name in MalwarePatternDetector.IOC_PACKAGES:
if dep_name in deps:
findings.append(f"🔗 依赖已知恶意包: {dep_name}@{deps[dep_name]}")
return findings
class FileSystemScanner:
"""文件系统扫描器"""
def __init__(self, target_path: str):
self.target_path = Path(target_path)
self.total_files = 0
self.suspicious_files = 0
def scan(self, recursive: bool = True) -> Dict[str, List[str]]:
"""执行扫描"""
results = {}
if self.target_path.is_file():
findings = self.scan_file(str(self.target_path))
if findings:
results[str(self.target_path)] = findings
elif self.target_path.is_dir():
pattern = '**/*' if recursive else '*'
for filepath in self.target_path.glob(pattern):
if filepath.is_file() and filepath.suffix in ['.js', '.json', '.node', '.ts']:
self.total_files += 1
findings = self.scan_file(str(filepath))
if findings:
results[str(filepath)] = findings
self.suspicious_files += 1
return results
def scan_file(self, filepath: str) -> List[str]:
"""扫描单个文件"""
try:
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
except Exception as e:
return [f"❌ 读取失败: {e}"]
all_findings = []
# 1. 如果是package.json,使用专用扫描器
if filepath.endswith('package.json'):
all_findings.extend(NPMPackageScanner.scan_package_json(filepath))
# 2. 扫描恶意模式
all_findings.extend(MalwarePatternDetector.scan_content(content, filepath))
# 3. 扫描PUA字符
pua_chars = UnicodePUADetector.detect(content)
if len(pua_chars) > 5: # 超过5个PUA字符视为可疑
all_findings.append(f"🚨 发现{len(pua_chars)}个Unicode PUA字符(高度可疑)")
# 尝试解码
decoded_b64 = UnicodePUADetector.decode_pua_to_base64(content)
if decoded_b64:
all_findings.append(f" 成功解码PUA为Base64: {decoded_b64[:40]}...")
try:
final_code = base64.b64decode(decoded_b64).decode('utf-8', errors='ignore')
all_findings.append(f" 最终解码: {final_code[:60]}...")
except:
pass
elif len(pua_chars) > 0:
all_findings.append(f"⚠️ 发现{len(pua_chars)}个PUA字符")
return all_findings
def print_report(results: Dict[str, List[str]], total_files: int, suspicious_files: int):
"""打印扫描报告"""
print("\\n" + "=" * 70)
print("NPM恶意软件扫描报告")
print("=" * 70)
print(f"扫描文件总数: {total_files}")
print(f"可疑文件数: {suspicious_files}")
print(f"威胁等级: {'🚨 HIGH' if suspicious_files > 0 else '✅ CLEAN'}")
print("=" * 70)
if not results:
print("\\n✅ 未发现明显恶意模式")
return
print(f"\\n发现 {len(results)} 个可疑文件:")
print("-" * 70)
for filepath, findings in results.items():
print(f"\\n📄 {filepath}")
for finding in findings:
print(f" {finding}")
print("\\n" + "=" * 70)
print("建议操作:")
if suspicious_files > 0:
print(" 1. 🚫 立即停止运行相关代码")
print(" 2. 🔍 人工审查标记的文件")
print(" 3. 🧹 从node_modules中删除恶意包")
print(" 4. 📝 检查package.json移除恶意依赖")
else:
print(" ✅ 未发现明显威胁,但仍建议保持警惕")
print("=" * 70)
def main():
"""主函数"""
import argparse
parser = argparse.ArgumentParser(
description='检测NPM供应链恶意软件(如os-info-checker-es6变种)',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
python malware_detector.py ./my-project
python malware_detector.py ./package.json
python malware_detector.py ./node_modules/os-info-checker-es6 --recursive
"""
)
parser.add_argument('path', help='要扫描的路径(文件或目录)')
parser.add_argument('-r', '--recursive', action='store_true',
help='递归扫描子目录(默认对目录启用)')
parser.add_argument('-v', '--verbose', action='store_true',
help='显示详细信息')
args = parser.parse_args()
if not os.path.exists(args.path):
print(f"❌ 错误: 路径不存在: {args.path}")
sys.exit(1)
print(f"🔍 开始扫描: {args.path}")
print(f"⏱️ 时间: {__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
scanner = FileSystemScanner(args.path)
results = scanner.scan(recursive=args.recursive)
print_report(results, scanner.total_files, scanner.suspicious_files)
# 返回码:发现威胁返回1,清洁返回0
sys.exit(1 if scanner.suspicious_files > 0 else 0)
if __name__ == "__main__":
main()
Convert web pages to PDF files using Playwright, saving them in A4 format with margins after fully loading the page.
---
name: web2pdf
description: Convert web pages to PDF files. Use when user wants to save a webpage as PDF, download a webpage offline, or create a PDF from a URL. Triggered by: "web to pdf", "convert url to pdf", "save webpage as pdf", "download webpage pdf".
---
# Web to PDF Converter
Convert web pages to PDF files using Playwright.
## Usage
```bash
python pdf2.py <url> [output_filename]
```
## Examples
```bash
# Convert a webpage to PDF (uses domain name as filename)
python pdf2.py https://example.com
# Specify custom output filename
python pdf2.py https://example.com my-document.pdf
# Short URL (https:// is added automatically)
python pdf2.py example.com output.pdf
```
## Setup
If playwright is not installed:
```bash
pip install playwright
playwright install chromium
```
## Script Location
The script is located at: `skills/web2pdf/scripts/pdf2.py`
## Notes
- The script uses Chromium via Playwright for accurate rendering
- Waits for network idle before PDF generation to ensure page is fully loaded
- Output is saved to the current working directory
- A4 format with 20px margins is used by default
FILE:scripts/pdf2.py
"""
Web to PDF converter using Playwright.
Usage: python pdf2.py <url> [output_filename]
"""
import sys
import os
from pathlib import Path
try:
from playwright.sync_api import sync_playwright
except ImportError:
print("Error: playwright not installed. Run: pip install playwright && playwright install chromium")
sys.exit(1)
def url_to_pdf(url: str, output: str = None) -> str:
"""Convert a web page to PDF."""
if not url:
print("Error: URL is required")
print("Usage: python pdf2.py <url> [output_filename]")
sys.exit(1)
# Validate URL
if not url.startswith(('http://', 'https://')):
url = 'https://' + url
# Determine output filename
if not output:
from urllib.parse import urlparse
domain = urlparse(url).netloc.replace(':', '_').replace('.', '_')
output = f"{domain}.pdf"
# Ensure .pdf extension
if not output.endswith('.pdf'):
output += '.pdf'
print(f"Converting: {url}")
print(f"Output: {output}")
try:
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto(url, wait_until='load', timeout=60000)
# Generate PDF
page.pdf(
path=output,
format='A4',
print_background=True,
margin={
'top': '20px',
'bottom': '20px',
'left': '20px',
'right': '20px'
}
)
browser.close()
print(f"[OK] PDF saved: {os.path.abspath(output)}")
return output
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python pdf2.py <url> [output_filename]")
print("Example: python pdf2.py https://example.com myfile.pdf")
sys.exit(1)
url = sys.argv[1]
output = sys.argv[2] if len(sys.argv) > 2 else None
url_to_pdf(url, output)
Extract readable text, markdown, HTML, JSON, or XML content from web pages using the trafilatura CLI tool with optional metadata and output formatting.
--- name: tra-extract-text description: Extract text content from web pages using trafilatura CLI. Use when user wants to extract readable text, markdown, or raw HTML from a URL. Triggers on requests like: "extract text from URL", "scrape web page content", "get article text", "convert web page to markdown", "trafilatura". --- # tra-extract-text Extract text from web pages using the `trafilatura` command-line tool. ## Installation ```bash pip install trafilatura ``` ## Usage ### Basic text extraction (Markdown) ```bash trafilatura -u URL --markdown ``` ### Extract raw text (no formatting) ```bash trafilatura -u URL --text ``` ### Output to file ```bash trafilatura -u URL --markdown > output.md trafilatura -u URL --text > output.txt ``` ### CLI Options | Option | Description | |--------|-------------| | `-u, --url` | Target URL (required) | | `--markdown` | Output as Markdown (default) | | `--text` | Output as plain text | | `--html` | Output as HTML | | `--json` | Output as JSON | | `--xml` | Output as XML | | `-o, --output` | Write to file instead of stdout | | `--with-metadata` | Include metadata (title, author, date) | | `--license` | Show license info | ## Examples Extract a Medium article to markdown: ```bash trafilatura -u "https://medium.com/example/article" --markdown ``` Extract and save: ```bash trafilatura -u "https://news.example.com/article" --markdown -o article.md ``` Extract with metadata: ```bash trafilatura -u "https://example.com/post" --markdown --with-metadata ```
任务管理 CLI 工具,用于 Super Productivity 数据的命令行管理。支持任务管理、计数器管理、项目管理、今日状态查看等功能。当用户提到 super-productivity、sp 任务管理、命令行任务管理时触发。
--- name: super-productivity description: 任务管理 CLI 工具,用于 Super Productivity 数据的命令行管理。支持任务管理、计数器管理、项目管理、今日状态查看等功能。当用户提到 super-productivity、sp 任务管理、命令行任务管理时触发。 --- # Super Productivity CLI 命令行接口,用于管理 Super Productivity 应用中的任务、计数器和项目。 ## 安装 ```bash pip install super-productivity-cli ``` ## 核心命令 ### 今日状态 ```bash sp status ``` 查看今天的任务摘要和统计数据。 ### 任务管理 (sp task) | 命令 | 说明 | |------|------| | `sp task list` | 列出所有任务 | | `sp task search <关键词>` | 搜索任务 | | `sp task view <task-id>` | 查看任务详情 | | `sp task add --title "<标题>"` | 添加新任务 | | `sp task edit <task-id> --title "<新标题>"` | 编辑任务 | | `sp task done <task-id>` | 标记任务完成 | | `sp task estimate <task-id> <时间>` | 估算时间,如 `2h`、`30m` | | `sp task log <task-id> <时间>` | 记录实际花费时间 | | `sp task plan <task-id> <日期> <时间>` | 计划任务时间 | | `sp task delete <task-id>` | 删除任务 | ### 计数器管理 (sp counter) | 命令 | 说明 | |------|------| | `sp counter list` | 列出所有计数器 | | `sp counter search <关键词>` | 搜索计数器 | | `sp counter add --title "<名称>"` | 添加计数器 | | `sp counter edit <counter-id> --title "<新名称>"` | 编辑计数器 | | `sp counter log <counter-id>` | 记录一次计数 | | `sp counter toggle <counter-id>` | 切换计数器状态 | | `sp counter delete <counter-id>` | 删除计数器 | ### 项目管理 (sp project) ```bash sp project list sp project view <project-id> ``` ## 输出选项 - `--json`: JSON 格式输出 - `--ndjson`: NDJSON 格式输出(每行一个 JSON 对象) - `--full`: 包含完整实体数据 ## 工作流示例 1. 查看今日任务:`sp status` 2. 搜索任务:`sp task search "报告"` 3. 查看任务详情:`sp task view <task-id>` 4. 标记完成:`sp task done <task-id>` ## 注意事项 - 所有修改命令需要使用 ID,不是标题模糊匹配 - 先用 `list` 或 `search` 获取 ID - 支持通配符 `*` 搜索,如 `sp task search "open*"` - **Windows 兼容性问题**: - 该包在 Python 3.11 上有 f-string 语法兼容问题(需手动修复源码) - Windows 控制台默认编码为 GBK,使用 `--json` 输出避免中文乱码 - 可通过设置环境变量 `PYTHONIOENCODING=utf-8` 解决编码问题 - **数据初始化**:首次使用需创建数据文件或配置 rclone 进行云同步