test-repo/skills/mission-control-docs/lib/docs.sh
OpenClaw Bot 22aca2d095 refactor: Update skills to use API endpoints with machine token auth
- gantt-tasks: Replace Supabase REST with API calls using GANTT_MACHINE_TOKEN
- mission-control-docs: Replace Supabase REST with API calls using MC_MACHINE_TOKEN
- Both skills now follow API-centric architecture
- Updated SKILL.md documentation for both

This ensures consistency with the CLI auth pattern and provides
single source of truth through API endpoints.
2026-02-26 09:01:55 -06:00

288 lines
8.6 KiB
Bash

#!/bin/bash
# mission-control-docs library - Full CRUD for Mission Control documents
# Location: ~/.agents/skills/mission-control-docs/lib/docs.sh
# Refactored to use API endpoints with machine token auth
# Configuration
MC_API_URL="${MC_API_URL:-https://mission-control-rho-pink.vercel.app/api}"
MC_MACHINE_TOKEN="${MC_MACHINE_TOKEN:-}"
# Error handler
_error_exit() {
echo "$1" >&2
return 1
}
# Make API call with machine token
_api_call() {
local method="$1"
local endpoint="$2"
local data="${3:-}"
if [[ -z "$MC_MACHINE_TOKEN" ]]; then
_error_exit "MC_MACHINE_TOKEN not set"
return 1
fi
local url="${MC_API_URL}${endpoint}"
local curl_opts=(
-s
-H "Content-Type: application/json"
-H "Authorization: Bearer ${MC_MACHINE_TOKEN}"
)
if [[ -n "$data" ]]; then
curl_opts+=(-d "$data")
fi
curl "${curl_opts[@]}" -X "$method" "$url" 2>/dev/null
}
# Detect folder based on content keywords (auto-categorization)
_detect_folder() {
local CONTENT="$1"
local FOLDER="Research/"
local LOWER_CONTENT=$(echo "$CONTENT" | tr '[:upper:]' '[:lower:]')
if [[ "$LOWER_CONTENT" =~ (ai|agent|llm|claude|openclaw|machine learning|gpt|model) ]]; then
FOLDER="Research/AI & Agents/"
elif [[ "$LOWER_CONTENT" =~ (swift|ios|xcode|apple|app store|uikit|swiftui) ]]; then
FOLDER="Research/iOS Development/"
elif [[ "$LOWER_CONTENT" =~ (saas|business|startup|marketing|revenue|growth|sales) ]]; then
FOLDER="Research/Business & Marketing/"
elif [[ "$LOWER_CONTENT" =~ (tool|library|api|package|sdk|framework|npm|pip) ]]; then
FOLDER="Research/Tools & Tech/"
elif [[ "$LOWER_CONTENT" =~ (tutorial|guide|how.to|walkthrough|step.by.step) ]]; then
FOLDER="Research/Tutorials/"
fi
echo "$FOLDER"
}
# Detect tags based on content keywords
_detect_tags() {
local CONTENT="$1"
local TAGS=("saved" "article")
local LOWER_CONTENT=$(echo "$CONTENT" | tr '[:upper:]' '[:lower:]')
[[ "$LOWER_CONTENT" =~ (ai|agent|automation|llm|model) ]] && TAGS+=("ai" "agents")
[[ "$LOWER_CONTENT" =~ openclaw ]] && TAGS+=("openclaw")
[[ "$LOWER_CONTENT" =~ (swift|ios) ]] && TAGS+=("ios" "swift")
[[ "$LOWER_CONTENT" =~ (business|saas) ]] && TAGS+=("business")
[[ "$LOWER_CONTENT" =~ tutorial ]] && TAGS+=("tutorial")
[[ "$LOWER_CONTENT" =~ (reference|documentation|docs) ]] && TAGS+=("reference")
printf '%s\n' "${TAGS[@]}" | jq -R . | jq -s 'unique'
}
# Create document
mc_doc_create() {
local TITLE=""
local CONTENT=""
local FOLDER=""
local TAGS=""
local TYPE="markdown"
local DESCRIPTION=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--title) TITLE="$2"; shift 2 ;;
--content) CONTENT="$2"; shift 2 ;;
--folder) FOLDER="$2"; shift 2 ;;
--tags) TAGS="$2"; shift 2 ;;
--type) TYPE="$2"; shift 2 ;;
--description) DESCRIPTION="$2"; shift 2 ;;
*) shift ;;
esac
done
# Validate
[[ -z "$TITLE" ]] && _error_exit "--title is required" && return 1
[[ -z "$CONTENT" ]] && _error_exit "--content is required" && return 1
# Clean content - remove control characters that break JSON
local CLEAN_CONTENT=$(echo "$CONTENT" | tr -d '\000-\031' | sed 's/\\/\\\\/g')
# Auto-detect folder/tags if not provided
[[ -z "$FOLDER" ]] && FOLDER=$(_detect_folder "$CONTENT")
[[ -z "$TAGS" ]] && TAGS=$(_detect_tags "$CONTENT")
[[ -z "$DESCRIPTION" ]] && DESCRIPTION="Created: $(date +%Y-%m-%d)"
# Build JSON payload
local TAGS_JSON
if [[ -n "$TAGS" ]]; then
TAGS_JSON="$TAGS"
else
TAGS_JSON=$(_detect_tags "$CONTENT")
fi
local PAYLOAD=$(jq -n \
--arg title "$TITLE" \
--arg content "$CLEAN_CONTENT" \
--arg type "$TYPE" \
--arg folder "$FOLDER" \
--argjson tags "$TAGS_JSON" \
--arg description "$DESCRIPTION" \
'{
title: $title,
content: $content,
type: $type,
folder: $folder,
tags: $tags,
description: $description
}')
# Create via API
local RESPONSE=$(_api_call "POST" "/documents" "$PAYLOAD")
local DOC_ID=$(echo "$RESPONSE" | jq -r '.id // .document?.id // empty')
if [[ -n "$DOC_ID" && "$DOC_ID" != "null" ]]; then
echo "$DOC_ID"
return 0
fi
_error_exit "Failed to create document: $(echo "$RESPONSE" | jq -r '.error // "Unknown error"')"
return 1
}
# Get document by ID
mc_doc_get() {
local DOC_ID="$1"
local RESPONSE=$(_api_call "GET" "/documents?id=$DOC_ID")
echo "$RESPONSE" | jq --arg id "$DOC_ID" '.documents | map(select(.id == $id)) | .[0] // empty'
}
# List documents with filters
mc_doc_list() {
local FOLDER=""
local TAG=""
local SEARCH=""
local LIMIT=50
local JSON_OUTPUT=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--folder) FOLDER="$2"; shift 2 ;;
--tags) TAG="$2"; shift 2 ;;
--search) SEARCH="$2"; shift 2 ;;
--limit) LIMIT="$2"; shift 2 ;;
--json) JSON_OUTPUT=true; shift ;;
*) shift ;;
esac
done
# Get documents via API
local RESPONSE=$(_api_call "GET" "/documents")
local DOCUMENTS=$(echo "$RESPONSE" | jq '.documents // []')
# Apply filters in jq
if [[ -n "$FOLDER" ]]; then
DOCUMENTS=$(echo "$DOCUMENTS" | jq --arg f "$FOLDER" 'map(select(.folder == $f))')
fi
if [[ -n "$TAG" ]]; then
DOCUMENTS=$(echo "$DOCUMENTS" | jq --arg t "$TAG" 'map(select(.tags | index($t)))')
fi
if [[ -n "$SEARCH" ]]; then
DOCUMENTS=$(echo "$DOCUMENTS" | jq --arg s "$SEARCH" 'map(select(.title | contains($s) or .content | contains($s)))')
fi
# Apply limit
DOCUMENTS=$(echo "$DOCUMENTS" | jq --argjson limit "$LIMIT" '.[:$limit]')
if [[ "$JSON_OUTPUT" == true ]]; then
echo "{\"documents\": $DOCUMENTS}"
else
echo "$DOCUMENTS" | jq -r '.[] | "\(.folder) | \(.title) | \(.id)"' 2>/dev/null
fi
}
# Update document
mc_doc_update() {
local DOC_ID="$1"
shift
# First get the current document
local CURRENT_DOC=$(mc_doc_get "$DOC_ID")
[[ -z "$CURRENT_DOC" ]] && _error_exit "Document not found: $DOC_ID" && return 1
# Build updates
local UPDATES="{}"
while [[ $# -gt 0 ]]; do
case $1 in
--title) UPDATES=$(echo "$UPDATES" | jq --arg v "$2" '. + {title: $v}'); shift 2 ;;
--content) UPDATES=$(echo "$UPDATES" | jq --arg v "$2" '. + {content: $v}'); shift 2 ;;
--folder) UPDATES=$(echo "$UPDATES" | jq --arg v "$2" '. + {folder: $v}'); shift 2 ;;
--tags) UPDATES=$(echo "$UPDATES" | jq --argjson v "$2" '. + {tags: $v}'); shift 2 ;;
--description) UPDATES=$(echo "$UPDATES" | jq --arg v "$2" '. + {description: $v}'); shift 2 ;;
*) shift ;;
esac
done
# Merge current document with updates
local MERGED_DOC=$(echo "$CURRENT_DOC" | jq --argjson updates "$UPDATES" '. + $updates')
# Send update via API
local PAYLOAD=$(jq -n --arg id "$DOC_ID" --argjson doc "$MERGED_DOC" '{id: $id, document: $doc}')
local RESPONSE=$(_api_call "PATCH" "/documents" "$PAYLOAD")
if echo "$RESPONSE" | jq -e '.error' >/dev/null 2>&1; then
_error_exit "Update failed: $(echo "$RESPONSE" | jq -r '.error')"
return 1
fi
echo "✅ Document updated"
}
# Delete document
mc_doc_delete() {
local DOC_ID="$1"
_api_call "DELETE" "/documents" "{\"id\": \"$DOC_ID\"}" >/dev/null
echo "✅ Document deleted"
}
# Search documents
mc_doc_search() {
local QUERY="$1"
local LIMIT="${2:-20}"
# Get documents and filter client-side
local RESPONSE=$(_api_call "GET" "/documents")
echo "$RESPONSE" | jq --arg q "$QUERY" --argjson limit "$LIMIT" '
.documents
| map(select(.title | test($q; "i") or .content | test($q; "i")))
| .[:$limit]
'
}
# List folders
mc_doc_folder_list() {
# Get all documents and extract unique folders
local RESPONSE=$(_api_call "GET" "/documents")
echo "$RESPONSE" | jq -r '.documents[].folder' | sort -u
}
# Export functions
export -f _detect_folder
export -f _detect_tags
export -f _api_call
export -f mc_doc_create
export -f mc_doc_get
export -f mc_doc_list
export -f mc_doc_update
export -f mc_doc_delete
export -f mc_doc_search
export -f mc_doc_folder_list