diff --git a/workspace/skills/context7/.env b/workspace/skills/context7/.env new file mode 100644 index 0000000..ce76b83 --- /dev/null +++ b/workspace/skills/context7/.env @@ -0,0 +1 @@ +CONTEXT7_API_KEY=ctx7sk-dfce3619-d4cf-4ce2-a1a1-9d0ba9d071ad diff --git a/workspace/skills/context7/SKILL.md b/workspace/skills/context7/SKILL.md new file mode 100644 index 0000000..8ce322e --- /dev/null +++ b/workspace/skills/context7/SKILL.md @@ -0,0 +1,19 @@ +--- +name: context7 +description: Intelligent documentation search and context using context7 service. +--- + +# Context7 + +Query documentation and repositories for intelligent context. + +## Usage + +```bash +npx tsx /root/.clawgo/skills/context7/query.ts context +``` + +Example: +```bash +npx tsx /root/.clawgo/skills/context7/query.ts context YspCoder/clawgo "How does the skill system work?" +``` diff --git a/workspace/skills/context7/package.json b/workspace/skills/context7/package.json new file mode 100644 index 0000000..67310b2 --- /dev/null +++ b/workspace/skills/context7/package.json @@ -0,0 +1,20 @@ +{ + "name": "context7-cli", + "version": "1.0.0", + "description": "Context7 MCP CLI for querying documentation", + "type": "module", + "scripts": { + "query": "tsx query.ts", + "read": "tsx read.ts", + "explore": "tsx explore.ts" + }, + "dependencies": { + "mcp-client": "^1.13.1", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "tsx": "^4.6.0", + "typescript": "^5.3.0" + } +} diff --git a/workspace/skills/context7/query.ts b/workspace/skills/context7/query.ts new file mode 100644 index 0000000..f596d41 --- /dev/null +++ b/workspace/skills/context7/query.ts @@ -0,0 +1,161 @@ +#!/usr/bin/env tsx +/** + * Context7 Query CLI + * + * Query Context7 API to search documentation and repository code. + * + * Usage: + * Search: npx tsx query.ts search + * Context: npx tsx query.ts context + * + * Examples: + * npx tsx query.ts search "better-auth/better-auth" "signIn social redirect callback" + * npx tsx query.ts context "facebook/react" "useState hook" + */ + +import { readFileSync } from "fs"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; + +// Load API key from .env file in the same directory as this script +const __dirname = dirname(fileURLToPath(import.meta.url)); +const envPath = join(__dirname, ".env"); +let API_KEY = process.env.CONTEXT7_API_KEY; + +// Try to load from .env file if not in environment +if (!API_KEY) { + try { + const envContent = readFileSync(envPath, "utf-8"); + const match = envContent.match(/CONTEXT7_API_KEY=(.+)/); + API_KEY = match?.[1]?.trim(); + } catch { + // .env file doesn't exist, continue with null + } +} + +if (!API_KEY) { + console.error("Error: CONTEXT7_API_KEY not found"); + console.error("Set it in environment or in .env file in this directory"); + process.exit(1); +} + +const command = process.argv[2]; +const repoName = process.argv[3]; +const query = process.argv[4]; + +// Help text +if (!command || command === "--help" || command === "-h") { + console.log(` +Context7 Query CLI + +Usage: + npx tsx query.ts + +Commands: + search Search for libraries by name with intelligent LLM-powered ranking + context Retrieve intelligent, LLM-reranked documentation context + +Examples: + npx tsx query.ts search "nextjs" "setup ssr" + npx tsx query.ts context "better-auth/better-auth" "signIn social redirect" + +For more info: https://context7.com/docs +`); + process.exit(0); +} + +if (!repoName || !query) { + console.error("Error: Missing arguments"); + console.error("Usage:"); + console.error(" Search: npx tsx query.ts search "); + console.error(" Context: npx tsx query.ts context "); + process.exit(1); +} + +// Ensure repo name starts with / +const libraryId = repoName.startsWith("/") ? repoName : `/${repoName}`; + +async function searchLibraries() { + try { + console.log(`Searching Context7 for libraries matching "${query}"...`); + + // Context7 Search API + const url = new URL("https://context7.com/api/v2/libs/search"); + url.searchParams.set("libraryName", libraryId.split("/")[1] || ""); + url.searchParams.set("query", query); + + const response = await fetch(url.toString(), { + headers: { + "Authorization": `Bearer ${API_KEY}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + console.error(`Context7 API error (${response.status}):`, error); + process.exit(1); + } + + const data = await response.json(); + + console.log("\n=== Search Results ===\n"); + + if (Array.isArray(data) && data.length > 0) { + data.forEach((lib: any, i: number) => { + console.log(`${i + 1}. ${lib.name || lib.id}`); + console.log(` Trust Score: ${lib.trustScore || "N/A"}`); + console.log(` Benchmark: ${lib.benchmarkScore || "N/A"}`); + if (lib.versions) { + console.log(` Versions: ${lib.versions.slice(0, 5).join(", ")}${lib.versions.length > 5 ? "..." : ""}`); + } + console.log(""); + }); + } else { + console.log("No results found."); + } + } catch (error: any) { + console.error("Error searching Context7:", error.message); + process.exit(1); + } +} + +async function getContext() { + try { + console.log(`Getting context for: "${query}" in ${libraryId}...`); + + // Context7 REST API + const url = new URL("https://context7.com/api/v2/context"); + url.searchParams.set("libraryId", libraryId); + url.searchParams.set("query", query); + url.searchParams.set("type", "txt"); + + const response = await fetch(url.toString(), { + headers: { + "Authorization": `Bearer ${API_KEY}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + console.error(`Context7 API error (${response.status}):`, error); + process.exit(1); + } + + const text = await response.text(); + console.log("\n=== Context Results ===\n"); + console.log(text); + } catch (error: any) { + console.error("Error querying Context7:", error.message); + process.exit(1); + } +} + +if (command === "search" || command === "s") { + searchLibraries(); +} else if (command === "context" || command === "c") { + getContext(); +} else { + console.error(`Unknown command: ${command}`); + console.error("Use 'search' or 'context'"); + process.exit(1); +} diff --git a/workspace/skills/tmux/SKILL.md b/workspace/skills/tmux/SKILL.md new file mode 100644 index 0000000..6502959 --- /dev/null +++ b/workspace/skills/tmux/SKILL.md @@ -0,0 +1,135 @@ +--- +name: tmux +description: Remote-control tmux sessions for interactive CLIs by sending keystrokes and scraping pane output. +metadata: + { "openclaw": { "emoji": "🧵", "os": ["darwin", "linux"], "requires": { "bins": ["tmux"] } } } +--- + +# tmux Skill (OpenClaw) + +Use tmux only when you need an interactive TTY. Prefer exec background mode for long-running, non-interactive tasks. + +## Quickstart (isolated socket, exec tool) + +```bash +SOCKET_DIR="${OPENCLAW_TMUX_SOCKET_DIR:-${CLAWDBOT_TMUX_SOCKET_DIR:-${TMPDIR:-/tmp}/openclaw-tmux-sockets}}" +mkdir -p "$SOCKET_DIR" +SOCKET="$SOCKET_DIR/openclaw.sock" +SESSION=openclaw-python + +tmux -S "$SOCKET" new -d -s "$SESSION" -n shell +tmux -S "$SOCKET" send-keys -t "$SESSION":0.0 -- 'PYTHON_BASIC_REPL=1 python3 -q' Enter +tmux -S "$SOCKET" capture-pane -p -J -t "$SESSION":0.0 -S -200 +``` + +After starting a session, always print monitor commands: + +``` +To monitor: + tmux -S "$SOCKET" attach -t "$SESSION" + tmux -S "$SOCKET" capture-pane -p -J -t "$SESSION":0.0 -S -200 +``` + +## Socket convention + +- Use `OPENCLAW_TMUX_SOCKET_DIR` (legacy `CLAWDBOT_TMUX_SOCKET_DIR` also supported). +- Default socket path: `"$OPENCLAW_TMUX_SOCKET_DIR/openclaw.sock"`. + +## Targeting panes and naming + +- Target format: `session:window.pane` (defaults to `:0.0`). +- Keep names short; avoid spaces. +- Inspect: `tmux -S "$SOCKET" list-sessions`, `tmux -S "$SOCKET" list-panes -a`. + +## Finding sessions + +- List sessions on your socket: `{baseDir}/scripts/find-sessions.sh -S "$SOCKET"`. +- Scan all sockets: `{baseDir}/scripts/find-sessions.sh --all` (uses `OPENCLAW_TMUX_SOCKET_DIR`). + +## Sending input safely + +- Prefer literal sends: `tmux -S "$SOCKET" send-keys -t target -l -- "$cmd"`. +- Control keys: `tmux -S "$SOCKET" send-keys -t target C-c`. +- For interactive TUI apps like Claude Code/Codex, this guidance covers **how to send commands**. + Do **not** append `Enter` in the same `send-keys`. These apps may treat a fast text+Enter + sequence as paste/multi-line input and not submit; this is timing-dependent. Send text and + `Enter` as separate commands with a small delay (tune per environment; increase if needed, + or use `sleep 1` if sub-second sleeps aren't supported): + +```bash +tmux -S "$SOCKET" send-keys -t target -l -- "$cmd" && sleep 0.1 && tmux -S "$SOCKET" send-keys -t target Enter +``` + +## Watching output + +- Capture recent history: `tmux -S "$SOCKET" capture-pane -p -J -t target -S -200`. +- Wait for prompts: `{baseDir}/scripts/wait-for-text.sh -t session:0.0 -p 'pattern'`. +- Attaching is OK; detach with `Ctrl+b d`. + +## Spawning processes + +- For python REPLs, set `PYTHON_BASIC_REPL=1` (non-basic REPL breaks send-keys flows). + +## Windows / WSL + +- tmux is supported on macOS/Linux. On Windows, use WSL and install tmux inside WSL. +- This skill is gated to `darwin`/`linux` and requires `tmux` on PATH. + +## Orchestrating Coding Agents (Codex, Claude Code) + +tmux excels at running multiple coding agents in parallel: + +```bash +SOCKET="${TMPDIR:-/tmp}/codex-army.sock" + +# Create multiple sessions +for i in 1 2 3 4 5; do + tmux -S "$SOCKET" new-session -d -s "agent-$i" +done + +# Launch agents in different workdirs +tmux -S "$SOCKET" send-keys -t agent-1 "cd /tmp/project1 && codex --yolo 'Fix bug X'" Enter +tmux -S "$SOCKET" send-keys -t agent-2 "cd /tmp/project2 && codex --yolo 'Fix bug Y'" Enter + +# When sending prompts to Claude Code/Codex TUI, split text + Enter with a delay +tmux -S "$SOCKET" send-keys -t agent-1 -l -- "Please make a small edit to README.md." && sleep 0.1 && tmux -S "$SOCKET" send-keys -t agent-1 Enter + +# Poll for completion (check if prompt returned) +for sess in agent-1 agent-2; do + if tmux -S "$SOCKET" capture-pane -p -t "$sess" -S -3 | grep -q "❯"; then + echo "$sess: DONE" + else + echo "$sess: Running..." + fi +done + +# Get full output from completed session +tmux -S "$SOCKET" capture-pane -p -t agent-1 -S -500 +``` + +**Tips:** + +- Use separate git worktrees for parallel fixes (no branch conflicts) +- `pnpm install` first before running codex in fresh clones +- Check for shell prompt (`❯` or `$`) to detect completion +- Codex needs `--yolo` or `--full-auto` for non-interactive fixes + +## Cleanup + +- Kill a session: `tmux -S "$SOCKET" kill-session -t "$SESSION"`. +- Kill all sessions on a socket: `tmux -S "$SOCKET" list-sessions -F '#{session_name}' | xargs -r -n1 tmux -S "$SOCKET" kill-session -t`. +- Remove everything on the private socket: `tmux -S "$SOCKET" kill-server`. + +## Helper: wait-for-text.sh + +`{baseDir}/scripts/wait-for-text.sh` polls a pane for a regex (or fixed string) with a timeout. + +```bash +{baseDir}/scripts/wait-for-text.sh -t session:0.0 -p 'pattern' [-F] [-T 20] [-i 0.5] [-l 2000] +``` + +- `-t`/`--target` pane target (required) +- `-p`/`--pattern` regex to match (required); add `-F` for fixed string +- `-T` timeout seconds (integer, default 15) +- `-i` poll interval seconds (default 0.5) +- `-l` history lines to search (integer, default 1000) diff --git a/workspace/skills/tmux/scripts/find-sessions.sh b/workspace/skills/tmux/scripts/find-sessions.sh new file mode 100755 index 0000000..00552c6 --- /dev/null +++ b/workspace/skills/tmux/scripts/find-sessions.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: find-sessions.sh [-L socket-name|-S socket-path|-A] [-q pattern] + +List tmux sessions on a socket (default tmux socket if none provided). + +Options: + -L, --socket tmux socket name (passed to tmux -L) + -S, --socket-path tmux socket path (passed to tmux -S) + -A, --all scan all sockets under NANOBOT_TMUX_SOCKET_DIR + -q, --query case-insensitive substring to filter session names + -h, --help show this help +USAGE +} + +socket_name="" +socket_path="" +query="" +scan_all=false +socket_dir="${NANOBOT_TMUX_SOCKET_DIR:-${TMPDIR:-/tmp}/nanobot-tmux-sockets}" + +while [[ $# -gt 0 ]]; do + case "$1" in + -L|--socket) socket_name="${2-}"; shift 2 ;; + -S|--socket-path) socket_path="${2-}"; shift 2 ;; + -A|--all) scan_all=true; shift ;; + -q|--query) query="${2-}"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage; exit 1 ;; + esac +done + +if [[ "$scan_all" == true && ( -n "$socket_name" || -n "$socket_path" ) ]]; then + echo "Cannot combine --all with -L or -S" >&2 + exit 1 +fi + +if [[ -n "$socket_name" && -n "$socket_path" ]]; then + echo "Use either -L or -S, not both" >&2 + exit 1 +fi + +if ! command -v tmux >/dev/null 2>&1; then + echo "tmux not found in PATH" >&2 + exit 1 +fi + +list_sessions() { + local label="$1"; shift + local tmux_cmd=(tmux "$@") + + if ! sessions="$("${tmux_cmd[@]}" list-sessions -F '#{session_name}\t#{session_attached}\t#{session_created_string}' 2>/dev/null)"; then + echo "No tmux server found on $label" >&2 + return 1 + fi + + if [[ -n "$query" ]]; then + sessions="$(printf '%s\n' "$sessions" | grep -i -- "$query" || true)" + fi + + if [[ -z "$sessions" ]]; then + echo "No sessions found on $label" + return 0 + fi + + echo "Sessions on $label:" + printf '%s\n' "$sessions" | while IFS=$'\t' read -r name attached created; do + attached_label=$([[ "$attached" == "1" ]] && echo "attached" || echo "detached") + printf ' - %s (%s, started %s)\n' "$name" "$attached_label" "$created" + done +} + +if [[ "$scan_all" == true ]]; then + if [[ ! -d "$socket_dir" ]]; then + echo "Socket directory not found: $socket_dir" >&2 + exit 1 + fi + + shopt -s nullglob + sockets=("$socket_dir"/*) + shopt -u nullglob + + if [[ "${#sockets[@]}" -eq 0 ]]; then + echo "No sockets found under $socket_dir" >&2 + exit 1 + fi + + exit_code=0 + for sock in "${sockets[@]}"; do + if [[ ! -S "$sock" ]]; then + continue + fi + list_sessions "socket path '$sock'" -S "$sock" || exit_code=$? + done + exit "$exit_code" +fi + +tmux_cmd=(tmux) +socket_label="default socket" + +if [[ -n "$socket_name" ]]; then + tmux_cmd+=(-L "$socket_name") + socket_label="socket name '$socket_name'" +elif [[ -n "$socket_path" ]]; then + tmux_cmd+=(-S "$socket_path") + socket_label="socket path '$socket_path'" +fi + +list_sessions "$socket_label" "${tmux_cmd[@]:1}" diff --git a/workspace/skills/tmux/scripts/wait-for-text.sh b/workspace/skills/tmux/scripts/wait-for-text.sh new file mode 100755 index 0000000..56354be --- /dev/null +++ b/workspace/skills/tmux/scripts/wait-for-text.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: wait-for-text.sh -t target -p pattern [options] + +Poll a tmux pane for text and exit when found. + +Options: + -t, --target tmux target (session:window.pane), required + -p, --pattern regex pattern to look for, required + -F, --fixed treat pattern as a fixed string (grep -F) + -T, --timeout seconds to wait (integer, default: 15) + -i, --interval poll interval in seconds (default: 0.5) + -l, --lines number of history lines to inspect (integer, default: 1000) + -h, --help show this help +USAGE +} + +target="" +pattern="" +grep_flag="-E" +timeout=15 +interval=0.5 +lines=1000 + +while [[ $# -gt 0 ]]; do + case "$1" in + -t|--target) target="${2-}"; shift 2 ;; + -p|--pattern) pattern="${2-}"; shift 2 ;; + -F|--fixed) grep_flag="-F"; shift ;; + -T|--timeout) timeout="${2-}"; shift 2 ;; + -i|--interval) interval="${2-}"; shift 2 ;; + -l|--lines) lines="${2-}"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown option: $1" >&2; usage; exit 1 ;; + esac +done + +if [[ -z "$target" || -z "$pattern" ]]; then + echo "target and pattern are required" >&2 + usage + exit 1 +fi + +if ! [[ "$timeout" =~ ^[0-9]+$ ]]; then + echo "timeout must be an integer number of seconds" >&2 + exit 1 +fi + +if ! [[ "$lines" =~ ^[0-9]+$ ]]; then + echo "lines must be an integer" >&2 + exit 1 +fi + +if ! command -v tmux >/dev/null 2>&1; then + echo "tmux not found in PATH" >&2 + exit 1 +fi + +# End time in epoch seconds (integer, good enough for polling) +start_epoch=$(date +%s) +deadline=$((start_epoch + timeout)) + +while true; do + # -J joins wrapped lines, -S uses negative index to read last N lines + pane_text="$(tmux capture-pane -p -J -t "$target" -S "-${lines}" 2>/dev/null || true)" + + if printf '%s\n' "$pane_text" | grep $grep_flag -- "$pattern" >/dev/null 2>&1; then + exit 0 + fi + + now=$(date +%s) + if (( now >= deadline )); then + echo "Timed out after ${timeout}s waiting for pattern: $pattern" >&2 + echo "Last ${lines} lines from $target:" >&2 + printf '%s\n' "$pane_text" >&2 + exit 1 + fi + + sleep "$interval" +done