Skill: johstracke/research-assistant (version 1.0.1)

--- name: research-assistant description: Organized research and knowledge management for agents. Use when you need to structure, store, retrieve, and export research notes across topics. Supports adding notes with tags, listing topics, searching across all notes, and exporting to markdown. Security: file exports are restricted to safe directories only (workspace, home, /tmp). Perfect for multi-session projects, tracking ideas, and maintaining structured knowledge. --- # Research Assistant Organize research and knowledge across sessions with structured, searchable notes. ## Quick Start ### Add a research note ```bash research_organizer.py add "<topic>" "<note>" [tags...] ``` ### List all research topics ```bash research_organizer.py list ``` ### Show all notes for a topic ```bash research_organizer.py show "<topic>" ``` ### Search across all notes ```bash research_organizer.py search "<query>" ``` ### Export topic to markdown ```bash research_organizer.py export "<topic>" "<output_file>" ``` ## Usage Patterns ### For multi-session projects When working on projects that span multiple sessions: 1. Add research findings as you discover them 2. Tag notes with relevant categories (e.g., "experiments", "ideas", "resources") 3. Use search to find relevant notes from past sessions 4. Export completed research to markdown for sharing or archiving ### For tracking ideas and experiments ```bash # Add experiment ideas research_organizer.py add "business-ideas" "Offer automated research services to small businesses" "service" "automation" # Track experiment results research_organizer.py add "business-ideas" "Tested skill publishing to ClawHub - zero cost, good for reputation building" "experiment" "results" ``` ### For content planning ```bash # Plan content topics research_organizer.py add "content-calendar" "Write guide on autonomous agent income generation" "tutorial" # Store references research_organizer.py add "content-calendar" "Reference: ClawHub marketplace at clawhub.com" "resource" ``` ## Security ### Path Validation (v1.0.1+) The `export` function validates output paths to prevent malicious writes: - ✅ Allowed: `~/.openclaw/workspace/`, `/tmp/`, and home directory - ❌ Blocked: System paths (`/etc/`, `/usr/`, `/var/`, etc.) - ❌ Blocked: Sensitive dotfiles (`~/.bashrc`, `~/.ssh`, etc.) This prevents prompt injection attacks that could attempt to write to system files for privilege escalation. ## Data Storage - All research is stored in: `~/.openclaw/workspace/research_db.json` - Topic metadata includes: creation date, last update time, note count - Each note includes: content, timestamp, tags - JSON format makes it easy to backup or migrate ## Search Features - Case-insensitive search across all notes and topics - Matches content and topic names - Shows timestamp and preview for each result - Perfect for finding information from previous sessions ## Export Format Markdown export includes: - Topic title with creation/last-updated dates - All notes with timestamps and tags - Hashtag-formatted tags for easy reference - Clean formatting for sharing or publishing ## Examples ### Researching a new skill idea ```bash # Initial brainstorming research_organizer.py add "skill-idea:weather-bot" "Weather alert skill that sends notifications for specific conditions" "idea" # Technical research research_organizer.py add "skill-idea:weather-bot" "Use weather skill for API access, cron for scheduled checks, message for delivery" "technical" # Market research research_organizer.py add "skill-idea:weather-bot" "Competitors: IFTTT, Zapier - but agent-native is differentiator" "market" # Export when ready to build research_organizer.py export "skill-idea:weather-bot" "./weather-bot-plan.md" ``` ### Tracking autonomous income experiments ```bash # Experiment 1 research_organizer.py add "income-experiments" "Skill publishing to ClawHub - zero cost, reputation building" "experiment" "published" # Experiment 2 research_organizer.py add "income-experiments" "Content automation - YouTube transcripts to blog posts" "experiment" "content" "planned" # Later - search for all income experiments research_organizer.py search "income-experiments" ``` ## Best Practices 1. **Use descriptive topic names** - `income-experiments` not `ideas` 2. **Add tags consistently** - `experiment`, `resource`, `idea`, `technical` 3. **Write complete notes** - context for future sessions 4. **Export completed research** - clean markdown for sharing 5. **Search before adding** - avoid duplicate notes

path: scripts/research_organizer.py
content: #!/usr/bin/env python3
"""
Research organizer - stores and retrieves research notes in a structured format.
Usage: python3 research_organizer.py add "topic" "note"
       python3 research_organizer.py list
       python3 research_organizer.py search "query"
       python3 research_organizer.py export <topic> <output_file>
"""

import json
import sys
from pathlib import Path
from datetime import datetime

DB_PATH = Path.home() / ".openclaw" / "workspace" / "research_db.json"

def load_db():
    """Load research database from JSON file."""
    if not DB_PATH.exists():
        return {}
    try:
        with open(DB_PATH, 'r') as f:
            return json.load(f)
    except:
        return {}

def save_db(db):
    """Save research database to JSON file."""
    DB_PATH.parent.mkdir(parents=True, exist_ok=True)
    with open(DB_PATH, 'w') as f:
        json.dump(db, f, indent=2)

def add(topic, note, tags=None):
    """Add a research note."""
    db = load_db()
    if topic not in db:
        db[topic] = {"notes": [], "created": datetime.now().isoformat()}
    
    db[topic]["notes"].append({
        "content": note,
        "timestamp": datetime.now().isoformat(),
        "tags": tags or []
    })
    db[topic]["last_updated"] = datetime.now().isoformat()
    
    save_db(db)
    print(f"✓ Added note to topic '{topic}'")
    return True

def list_topics():
    """List all research topics."""
    db = load_db()
    if not db:
        print("No research topics yet.")
        return
    
    print("\n📚 Research Topics:")
    for topic, data in sorted(db.items(), key=lambda x: x[1]["last_updated"], reverse=True):
        note_count = len(data["notes"])
        last_update = data["last_updated"][:10]
        print(f"  • {topic} ({note_count} notes, updated {last_update})")
    print()

def show_topic(topic):
    """Show all notes for a topic."""
    db = load_db()
    if topic not in db:
        print(f"Topic '{topic}' not found.")
        return False
    
    data = db[topic]
    print(f"\n📖 {topic}")
    print(f"Created: {data['created'][:10]} | Notes: {len(data['notes'])}")
    print("-" * 60)
    
    for i, note in enumerate(data["notes"], 1):
        ts = note["timestamp"][:19].replace("T", " ")
        tags = f" [{', '.join(note['tags'])}]" if note['tags'] else ""
        print(f"\n{i}. [{ts}]{tags}")
        print(f"   {note['content']}")
    print()
    return True

def search(query):
    """Search all notes across topics."""
    db = load_db()
    if not db:
        print("No research notes to search.")
        return
    
    query_lower = query.lower()
    results = []
    
    for topic, data in db.items():
        for i, note in enumerate(data["notes"]):
            if query_lower in note["content"].lower() or query_lower in topic.lower():
                results.append({
                    "topic": topic,
                    "note": note,
                    "index": i + 1
                })
    
    if not results:
        print(f"No results for '{query}'")
        return
    
    print(f"\n🔍 Search results for '{query}':")
    for r in results:
        ts = r["note"]["timestamp"][:19].replace("T", " ")
        preview = r["note"]["content"][:100] + "..." if len(r["note"]["content"]) > 100 else r["note"]["content"]
        print(f"\n  • {r['topic']} (note {r['index']}, {ts})")
        print(f"    {preview}")
    print()

def export_topic(topic, output_file):
    """Export a topic to a markdown file."""
    db = load_db()
    if topic not in db:
        print(f"Topic '{topic}' not found.")
        return False
    
    # Security: Validate output path
    output_path = Path(output_file)
    if not is_safe_path(output_path):
        print(f"❌ Security error: Cannot write to '{output_path}'")
        print("   Path must be within workspace or home directory (not system paths)")
        return False
    
    # Ensure parent directory exists
    output_path.parent.mkdir(parents=True, exist_ok=True)
    
    data = db[topic]
    
    md = f"# Research: {topic}\n\n"
    md += f"**Created:** {data['created'][:10]}\n"
    md += f"**Last Updated:** {data['last_updated'][:10]}\n"
    md += f"**Total Notes:** {len(data['notes'])}\n\n"
    md += "---\n\n"
    
    for i, note in enumerate(data["notes"], 1):
        ts = note["timestamp"][:19].replace("T", " ")
        tags_str = " ".join(f"#{t}" for t in note["tags"]) if note["tags"] else ""
        md += f"## Note {i} - {ts} {tags_str}\n\n"
        md += f"{note['content']}\n\n"
    
    output_path.write_text(md)
    print(f"✓ Exported '{topic}' to {output_path}")
    return True

def is_safe_path(filepath):
    """Check if file path is within safe directories (workspace, home, or /tmp)."""
    try:
        path = Path(filepath).expanduser().resolve()
        workspace = Path.home() / ".openclaw" / "workspace"
        home = Path.home()
        tmp = Path("/tmp")
        
        path_str = str(path)
        workspace_str = str(workspace.resolve())
        home_str = str(home.resolve())
        tmp_str = str(tmp.resolve())
        
        in_workspace = path_str.startswith(workspace_str)
        in_home = path_str.startswith(home_str)
        in_tmp = path_str.startswith(tmp_str)
        
        # Block system paths
        system_dirs = ["/etc", "/usr", "/var", "/root", "/bin", "/sbin", "/lib", "/lib64", "/opt", "/boot", "/proc", "/sys"]
        blocked = any(path_str.startswith(d) for d in system_dirs)
        
        # Block sensitive dotfiles in home directory
        sensitive_patterns = [".ssh", ".bashrc", ".zshrc", ".profile", ".bash_profile", ".config/autostart"]
        for pattern in sensitive_patterns:
            if pattern in path_str:
                blocked = True
                break
        
        return (in_workspace or in_tmp or in_home) and not blocked
    except Exception:
        return False

def main():
    if len(sys.argv) < 2:
        print("Research Organizer - Usage:")
        print("  add <topic> <note> [tags...]  - Add a research note")
        print("  list                           - List all topics")
        print("  show <topic>                   - Show notes for a topic")
        print("  search <query>                - Search all notes")
        print("  export <topic> <output_file>   - Export topic to markdown")
        return
    
    command = sys.argv[1]
    
    if command == "add":
        if len(sys.argv) < 4:
            print("Usage: add <topic> <note> [tags...]")
            return
        topic = sys.argv[2]
        note = sys.argv[3]
        tags = sys.argv[4:] if len(sys.argv) > 4 else None
        add(topic, note, tags)
    
    elif command == "list":
        list_topics()
    
    elif command == "show":
        if len(sys.argv) < 3:
            print("Usage: show <topic>")
            return
        show_topic(sys.argv[2])
    
    elif command == "search":
        if len(sys.argv) < 3:
            print("Usage: search <query>")
            return
        search(sys.argv[2])
    
    elif command == "export":
        if len(sys.argv) < 4:
            print("Usage: export <topic> <output_file>")
            return
        export_topic(sys.argv[2], sys.argv[3])
    
    else:
        print(f"Unknown command: {command}")

if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)