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.
This commit is contained in:
OpenClaw Bot 2026-02-26 09:01:55 -06:00
parent 81effddf10
commit 22aca2d095
4 changed files with 1406 additions and 0 deletions

357
skills/gantt-tasks/SKILL.md Normal file
View File

@ -0,0 +1,357 @@
---
name: gantt-tasks
description: Full CRUD operations for Gantt Board tasks including comments, attachments, and sprint management. Uses Gantt Board API with machine token authentication.
---
# gantt-tasks
**Module Skill** - Complete task management for Gantt Board via API.
## Purpose
Provides all operations needed to work with Gantt Board tasks: create, read, update, delete, comments, attachments, and sprint queries. Used by orchestrator skills and agents.
## Authentication
Uses **machine token** for server-to-server API calls:
```bash
export GANTT_MACHINE_TOKEN="50cd5e8fe3f895353f97c9ee64052c0b689b4eedf79259746413734d0a163cf8"
export GANTT_API_URL="https://gantt-board.vercel.app/api"
```
**Architecture:** Skills → API (with machine token) → Database
This follows the API-centric pattern where all business logic lives in API routes.
## Usage
```bash
source ~/.agents/skills/gantt-tasks/lib/tasks.sh
# Create task
TASK_ID=$(task_create --title "Fix bug" --project "Gantt Board" --sprint current)
# Add comment
task_add_comment "$TASK_ID" "Starting work on this"
# Attach file
task_attach_file "$TASK_ID" "/path/to/file.md"
```
## Functions
### Task CRUD
#### `task_create([options])`
Create a new task.
**Options:**
- `--title` - Task title (required)
- `--description` - Task description (optional)
- `--type` - Task type: feature, bug, research, task (default: task)
- `--status` - Status: open, todo, in-progress, review, done (default: open)
- `--priority` - Priority: low, medium, high, critical (default: medium)
- `--project` - Project name or ID (optional, auto-detects "current")
- `--sprint` - Sprint name or ID, or "current" for current sprint (optional)
- `--assignee` - Assignee name or ID (default: Max)
- `--due-date` - Due date YYYY-MM-DD (optional)
- `--tags` - Tags as comma-separated string (optional)
**Returns:** Task ID on success
**Example:**
```bash
TASK_ID=$(task_create \
--title "Research API options" \
--description "Compare REST vs GraphQL" \
--type research \
--priority high \
--sprint current \
--tags "api,research")
```
#### `task_get(TASK_ID)`
Get full task details including comments.
**Parameters:**
- `TASK_ID` - Task UUID
**Returns:** Task JSON
**Example:**
```bash
task=$(task_get "a1b2c3d4-...")
echo "$task" | jq -r '.title'
```
#### `task_update(TASK_ID, [options])`
Update task fields.
**Parameters:**
- `TASK_ID` - Task UUID
- Options same as task_create (except project/sprint usually fixed)
**Example:**
```bash
task_update "$TASK_ID" --status in-progress --priority critical
```
#### `task_delete(TASK_ID)`
Delete a task (permanent).
**Example:**
```bash
task_delete "$TASK_ID"
```
#### `task_list([filters])`
List tasks with optional filters.
**Filters:**
- `--status` - Filter by status (comma-separated: open,todo,in-progress)
- `--project` - Filter by project name/ID
- `--sprint` - Filter by sprint name/ID or "current"
- `--assignee` - Filter by assignee
- `--priority` - Filter by priority
- `--json` - Output as JSON
**Example:**
```bash
# List current sprint tasks
task_list --sprint current --status open,todo,in-progress
# List my high priority tasks
task_list --assignee Max --priority high
```
### Comments
#### `task_add_comment(TASK_ID, TEXT, [OPTIONS])`
Add a comment to a task.
**Parameters:**
- `TASK_ID` - Task UUID
- `TEXT` - Comment text (supports markdown)
- `--checklist` - Include checklist format automatically
- `--status` - Add status prefix (in-progress, review, etc.)
**Example:**
```bash
# Simple comment
task_add_comment "$TASK_ID" "Found the issue in line 42"
# Checklist comment with status
task_add_comment "$TASK_ID" \
"Fixed the bug\n\nNow testing..." \
--checklist \
--status in-progress
```
#### `task_update_comment(TASK_ID, COMMENT_ID, TEXT)`
Update an existing comment (for progress tracking).
**Parameters:**
- `TASK_ID` - Task UUID
- `COMMENT_ID` - Comment UUID to update
- `TEXT` - New comment text
**Example:**
```bash
# Update progress comment
COMMENT_ID=$(echo "$TASK" | jq -r '.comments[-1].id')
task_update_comment "$TASK_ID" "$COMMENT_ID" \
"## $(date +%Y-%m-%d) 🔄 In Progress\n\n### ✅ Completed\n- [x] Phase 1\n\n### 🔄 In Progress\n- [ ] Phase 2"
```
**Auto-format:**
If `--checklist` is passed, formats as:
```markdown
## [YYYY-MM-DD HH:MM] 🔄 [STATUS]
### ✅ Completed
- [x] Previous item
### 🔄 In Progress
- [x] Current item: [TEXT]
### 📋 Remaining
- [ ] Next step
### 📊 Progress: X/Y tasks complete
```
#### `task_reply_to_comment(TASK_ID, COMMENT_ID, TEXT)`
Reply to an existing comment.
**Example:**
```bash
task_reply_to_comment "$TASK_ID" "$COMMENT_ID" "Great point! I'll check that."
```
### Attachments
#### `task_attach_file(TASK_ID, FILE_PATH, [OPTIONS])`
Attach a file to a task.
**Parameters:**
- `TASK_ID` - Task UUID
- `FILE_PATH` - Path to file
- `--description` - Attachment description (optional)
**Example:**
```bash
# Attach markdown file
task_attach_file "$TASK_ID" "/tmp/plan.md" --description "Implementation plan"
# Attach any file type
task_attach_file "$TASK_ID" "/tmp/screenshot.png"
```
**Note:** Large files should be stored elsewhere and linked.
### Sprint Management
#### `sprint_get_current()`
Get the current sprint (contains today's date).
**Returns:** Sprint ID
**Example:**
```bash
SPRINT_ID=$(sprint_get_current)
echo "Current sprint: $SPRINT_ID"
```
#### `sprint_list([OPTIONS])`
List all sprints.
**Options:**
- `--active` - Only active sprints
- `--project` - Filter by project
**Example:**
```bash
sprint_list --active
```
### Project Resolution
#### `resolve_project_id(PROJECT_NAME)`
Convert project name to ID.
**Example:**
```bash
PROJECT_ID=$(resolve_project_id "Gantt Board")
```
#### `resolve_user_id(USER_NAME)`
Convert user name to ID.
**Example:**
```bash
USER_ID=$(resolve_user_id "Max")
```
## File Structure
```
~/.agents/skills/gantt-tasks/
├── SKILL.md # This documentation
└── lib/
└── tasks.sh # All task functions
```
## Integration Example
Complete workflow:
```bash
# Set authentication
export GANTT_MACHINE_TOKEN="50cd5e8fe3f895353f97c9ee64052c0b689b4eedf79259746413734d0a163cf8"
export GANTT_API_URL="https://gantt-board.vercel.app/api"
source ~/.agents/skills/gantt-tasks/lib/tasks.sh
# 1. Create task
TASK_ID=$(task_create \
--title "Research URL extraction options" \
--type research \
--priority high \
--sprint current)
echo "Created task: $TASK_ID"
# 2. Add initial comment
task_add_comment "$TASK_ID" "Starting research phase" --checklist
# 3. Update with progress
task_update "$TASK_ID" --status in-progress
task_add_comment "$TASK_ID" "Evaluated 3 options:\n1. Scrapling\n2. Tavily\n3. web_fetch" \
--checklist \
--status in-progress
# 4. Attach recommendation
cat > /tmp/recommendation.md << 'EOF'
## Recommendation
Use Scrapling as primary, Tavily as fallback.
- Scrapling handles JavaScript sites
- Tavily fast for articles
- web_fetch for simple sites
EOF
task_attach_file "$TASK_ID" /tmp/recommendation.md --description "Research findings"
# 5. Mark ready for review
task_update "$TASK_ID" --status review
task_add_comment "$TASK_ID" "Research complete. Recommendation attached." \
--checklist
echo "Task ready for review: https://gantt-board.vercel.app/tasks/$TASK_ID"
```
## CLI Alternative
For interactive/command-line use, use the Gantt Board CLI:
```bash
# Set auth for CLI
export GANTT_MACHINE_TOKEN="50cd5e8fe3f895353f97c9ee64052c0b689b4eedf79259746413734d0a163cf8"
export API_URL="https://gantt-board.vercel.app/api"
cd /Users/mattbruce/Documents/Projects/OpenClaw/Web/gantt-board
# Create task
./scripts/gantt.sh task create "Title" --type research --sprint current
# List tasks
./scripts/gantt.sh task list --sprint current --status in-progress
# Get task
./scripts/gantt.sh task get $TASK_ID
# Update task
./scripts/gantt.sh task update $TASK_ID --status review
```
**Note:** The skill library and CLI both use the same API endpoints with machine token authentication.
---
**Note:** This is a low-level module skill. Used by orchestrator skills, not directly by users.

View File

@ -0,0 +1,495 @@
#!/bin/bash
# gantt-tasks library - Full CRUD for Gantt Board tasks
# Location: ~/.agents/skills/gantt-tasks/lib/tasks.sh
# Refactored to use API endpoints with machine token auth
# Configuration
GANTT_API_URL="${GANTT_API_URL:-https://gantt-board.vercel.app/api}"
GANTT_MACHINE_TOKEN="${GANTT_MACHINE_TOKEN:-}"
MAX_ID="9c29cc99-81a1-4e75-8dff-cd7cc5ceb5aa" # Max user ID
# Debug function
debug() {
echo "[DEBUG] $*" >&2
}
# 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 "$GANTT_MACHINE_TOKEN" ]]; then
error_exit "GANTT_MACHINE_TOKEN not set"
return 1
fi
local url="${GANTT_API_URL}${endpoint}"
local curl_opts=(
-s
-H "Content-Type: application/json"
-H "Authorization: Bearer ${GANTT_MACHINE_TOKEN}"
)
if [[ -n "$data" ]]; then
curl_opts+=(-d "$data")
fi
curl "${curl_opts[@]}" -X "$method" "$url" 2>/dev/null
}
# Resolve project name to ID
resolve_project_id() {
local PROJECT_NAME="$1"
# Special case: Research Project
if [[ "$PROJECT_NAME" == "Research" || "$PROJECT_NAME" == "Research Project" ]]; then
echo "a1b2c3d4-0003-0000-0000-000000000003"
return 0
fi
# Query by name via API
local RESULT=$(_api_call "GET" "/projects" | jq -r --arg q "$PROJECT_NAME" '
.projects
| map(select((.name // "") | ascii_downcase | contains($q | ascii_downcase)))
| .[0].id // empty
')
if [[ -n "$RESULT" ]]; then
echo "$RESULT"
return 0
fi
# Return empty if not found
echo ""
return 1
}
# Resolve user name to ID
resolve_user_id() {
local USER_NAME="$1"
# Current mappings
case "$USER_NAME" in
"Max"|"max") echo "9c29cc99-81a1-4e75-8dff-cd7cc5ceb5aa" ;;
"Matt"|"matt"|"Matt Bruce") echo "0a3e400c-3932-48ae-9b65-f3f9c6f26fe9" ;;
*) echo "$MAX_ID" ;; # Default to Max
esac
}
# Get current sprint (contains today's date)
sprint_get_current() {
local TODAY=$(date +%Y-%m-%d)
# Get current sprint via API
local RESULT=$(_api_call "GET" "/sprints" | jq -r --arg today "$TODAY" '
.sprints
| map(select(.startDate <= $today and .endDate >= $today))
| .[0].id // empty
')
if [[ -n "$RESULT" && "$RESULT" != "null" ]]; then
echo "$RESULT"
return 0
fi
# Fallback: get most recent sprint
RESULT=$(_api_call "GET" "/sprints" | jq -r '
.sprints
| sort_by(.startDate)
| reverse
| .[0].id // empty
')
echo "$RESULT"
}
# Resolve sprint name/ID
resolve_sprint_id() {
local SPRINT="$1"
if [[ "$SPRINT" == "current" ]]; then
sprint_get_current
return 0
fi
# If it looks like a UUID, use it directly
if [[ "$SPRINT" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
echo "$SPRINT"
return 0
fi
# Query by name via API
local RESULT=$(_api_call "GET" "/sprints" | jq -r --arg q "$SPRINT" '
.sprints
| map(select((.name // "") | ascii_downcase | contains($q | ascii_downcase)))
| .[0].id // empty
')
echo "$RESULT"
}
# Create a new task
task_create() {
local TITLE=""
local DESCRIPTION=""
local TYPE="task"
local STATUS="open"
local PRIORITY="medium"
local PROJECT=""
local SPRINT=""
local ASSIGNEE="Max"
local DUE_DATE=""
local TAGS=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--title) TITLE="$2"; shift 2 ;;
--description) DESCRIPTION="$2"; shift 2 ;;
--type) TYPE="$2"; shift 2 ;;
--status) STATUS="$2"; shift 2 ;;
--priority) PRIORITY="$2"; shift 2 ;;
--project) PROJECT="$2"; shift 2 ;;
--sprint) SPRINT="$2"; shift 2 ;;
--assignee) ASSIGNEE="$2"; shift 2 ;;
--due-date) DUE_DATE="$2"; shift 2 ;;
--tags) TAGS="$2"; shift 2 ;;
*) shift ;;
esac
done
# Validate
[[ -z "$TITLE" ]] && error_exit "--title is required" && return 1
# Resolve IDs
local PROJECT_ID="a1b2c3d4-0003-0000-0000-000000000003"
if [[ -n "$PROJECT" ]]; then
PROJECT_ID=$(resolve_project_id "$PROJECT")
fi
local SPRINT_ID=""
if [[ -n "$SPRINT" ]]; then
SPRINT_ID=$(resolve_sprint_id "$SPRINT")
fi
local ASSIGNEE_ID=$(resolve_user_id "$ASSIGNEE")
# Build JSON payload
local JSONPayload=$(jq -n \
--arg title "$TITLE" \
--arg type "$TYPE" \
--arg status "$STATUS" \
--arg priority "$PRIORITY" \
--arg assigneeId "$ASSIGNEE_ID" \
--arg projectId "$PROJECT_ID" \
'{
title: $title,
type: $type,
status: $status,
priority: $priority,
assigneeId: $assigneeId,
projectId: $projectId,
comments: [],
tags: []
}')
# Add optional fields
[[ -n "$DESCRIPTION" ]] && JSONPayload=$(echo "$JSONPayload" | jq --arg desc "$DESCRIPTION" '. + {description: $desc}')
[[ -n "$SPRINT_ID" ]] && JSONPayload=$(echo "$JSONPayload" | jq --arg sid "$SPRINT_ID" '. + {sprintId: $sid}')
[[ -n "$DUE_DATE" ]] && JSONPayload=$(echo "$JSONPayload" | jq --arg dd "$DUE_DATE" '. + {dueDate: $dd}')
[[ -n "$TAGS" ]] && JSONPayload=$(echo "$JSONPayload" | jq --argjson tags "[$(echo "$TAGS" | sed 's/,/","/g' | sed 's/^/"/' | sed 's/$/"/')]" '. + {tags: $tags}')
# Create task via API
local Response=$(_api_call "POST" "/tasks" "{\"task\": $JSONPayload}")
local TaskId=$(echo "$Response" | jq -r '.task.id // .id // empty')
if [[ -n "$TaskId" && "$TaskId" != "null" ]]; then
echo "$TaskId"
return 0
fi
error_exit "Failed to create task: $(echo "$Response" | jq -r '.error // "Unknown error"')"
return 1
}
# Get task details
task_get() {
local TASK_ID="$1"
local Response=$(_api_call "GET" "/tasks")
echo "$Response" | jq --arg id "$TASK_ID" '.tasks | map(select(.id == $id)) | .[0] // empty'
}
# List tasks with filters
task_list() {
local StatusFilter=""
local ProjectFilter=""
local SprintFilter=""
local AssigneeFilter=""
local PriorityFilter=""
local JsonOutput=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--status) StatusFilter="$2"; shift 2 ;;
--project) ProjectFilter="$2"; shift 2 ;;
--sprint) SprintFilter="$2"; shift 2 ;;
--assignee) AssigneeFilter="$2"; shift 2 ;;
--priority) PriorityFilter="$2"; shift 2 ;;
--json) JsonOutput=true; shift ;;
*) shift ;;
esac
done
# Get all tasks via API
local Response=$(_api_call "GET" "/tasks")
local Tasks=$(echo "$Response" | jq '.tasks // []')
# Apply filters in jq
local Filtered="$Tasks"
if [[ -n "$StatusFilter" ]]; then
# Handle comma-separated status values
if [[ "$StatusFilter" =~ , ]]; then
Filtered=$(echo "$Filtered" | jq --arg statuses "$StatusFilter" '
[split(",")] as $statusList | map(select(.status as $s | $statusList | index($s)))
')
else
Filtered=$(echo "$Filtered" | jq --arg status "$StatusFilter" 'map(select(.status == $status))')
fi
fi
if [[ -n "$ProjectFilter" ]]; then
local ProjectId=$(resolve_project_id "$ProjectFilter")
[[ -n "$ProjectId" ]] && Filtered=$(echo "$Filtered" | jq --arg pid "$ProjectId" 'map(select(.projectId == $pid))')
fi
if [[ -n "$SprintFilter" ]]; then
local SprintId=$(resolve_sprint_id "$SprintFilter")
[[ -n "$SprintId" ]] && Filtered=$(echo "$Filtered" | jq --arg sid "$SprintId" 'map(select(.sprintId == $sid))')
fi
if [[ -n "$AssigneeFilter" ]]; then
local AssigneeId=$(resolve_user_id "$AssigneeFilter")
Filtered=$(echo "$Filtered" | jq --arg aid "$AssigneeId" 'map(select(.assigneeId == $aid))')
fi
if [[ -n "$PriorityFilter" ]]; then
Filtered=$(echo "$Filtered" | jq --arg pri "$PriorityFilter" 'map(select(.priority == $pri))')
fi
if [[ "$JsonOutput" == true ]]; then
echo "{\"tasks\": $Filtered}"
else
# Pretty print
echo "$Filtered" | jq -r '.[] | "\(.status) | \(.priority) | \(.title) | \(.id)"' 2>/dev/null
fi
}
# Update task
task_update() {
local TASK_ID="$1"
shift
# First get the current task
local CurrentTask=$(task_get "$TASK_ID")
[[ -z "$CurrentTask" ]] && error_exit "Task not found: $TASK_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 ;;
--description) Updates=$(echo "$Updates" | jq --arg v "$2" '. + {description: $v}'); shift 2 ;;
--status) Updates=$(echo "$Updates" | jq --arg v "$2" '. + {status: $v}'); shift 2 ;;
--priority) Updates=$(echo "$Updates" | jq --arg v "$2" '. + {priority: $v}'); shift 2 ;;
--due-date) Updates=$(echo "$Updates" | jq --arg v "$2" '. + {dueDate: $v}'); shift 2 ;;
*) shift ;;
esac
done
# Merge current task with updates
local MergedTask=$(echo "$CurrentTask" | jq --argjson updates "$Updates" '. + $updates')
# Send update via API
local Payload=$(jq -n --argjson task "$MergedTask" '{task: $task}')
local Response=$(_api_call "POST" "/tasks" "$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 "✅ Task updated"
}
# Delete task
task_delete() {
local TASK_ID="$1"
_api_call "DELETE" "/tasks" "{\"id\": \"$TASK_ID\"}" >/dev/null
echo "✅ Task deleted"
}
# Add comment to task
task_add_comment() {
local TASK_ID="$1"
local TEXT="$2"
local UseChecklist=false
local StatusPrefix=""
shift 2
while [[ $# -gt 0 ]]; do
case $1 in
--checklist) UseChecklist=true; shift ;;
--status) StatusPrefix="$2"; shift 2 ;;
*) shift ;;
esac
done
# Format comment
local Now=$(date +"%Y-%m-%d %H:%M")
local StatusEmoji="🔄"
[[ -n "$StatusPrefix" ]] && StatusEmoji="$StatusPrefix"
local CommentText
if [[ "$UseChecklist" == true ]]; then
CommentText=$(cat <<EOF
## [$Now] $StatusEmoji ${StatusPrefix:-Update}
### ✅ Completed
- [x] Previous work
### 🔄 In Progress
- [x] $TEXT
### 📋 Remaining
- [ ] Next step
### 📊 Progress: 1/1 tasks
EOF
)
else
CommentText="[$Now] $TEXT"
fi
# Get current task with comments
local CurrentTask=$(task_get "$TASK_ID")
local CurrentComments=$(echo "$CurrentTask" | jq '.comments // []')
# Build new comment
local NewComment=$(jq -n \
--arg id "$(uuidgen 2>/dev/null || date +%s)" \
--arg text "$CommentText" \
--arg author "$MAX_ID" \
'{
id: $id,
text: $text,
createdAt: now | todate,
commentAuthorId: $author,
replies: []
}')
# Append to comments
local UpdatedComments=$(echo "$CurrentComments" | jq --argjson new "$NewComment" '. + [$new]')
# Merge and update task
local MergedTask=$(echo "$CurrentTask" | jq --argjson comments "$UpdatedComments" '. + {comments: $comments}')
local Payload=$(jq -n --argjson task "$MergedTask" '{task: $task}')
_api_call "POST" "/tasks" "$Payload" >/dev/null
echo "✅ Comment added"
}
# Attach file to task (stores content in attachments field)
task_attach_file() {
local TASK_ID="$1"
local FILE_PATH="$2"
local Description="${3:-Attached file}"
[[ ! -f "$FILE_PATH" ]] && error_exit "File not found: $FILE_PATH" && return 1
# Read file content
local Content=$(cat "$FILE_PATH")
local FileName=$(basename "$FILE_PATH")
# Get current task with attachments
local CurrentTask=$(task_get "$TASK_ID")
local CurrentAttachments=$(echo "$CurrentTask" | jq '.attachments // []')
# Build new attachment
local NewAttachment=$(jq -n \
--arg id "$(uuidgen 2>/dev/null || date +%s)" \
--arg name "$FileName" \
--arg type "$(file -b --mime-type "$FILE_PATH" 2>/dev/null || echo 'application/octet-stream')" \
--arg dataUrl "data:text/plain;base64,$(base64 -i "$FILE_PATH" | tr -d '\n')" \
'{
id: $id,
name: $name,
type: $type,
dataUrl: $dataUrl,
uploadedAt: now | todate
}')
# Append to attachments
local UpdatedAttachments=$(echo "$CurrentAttachments" | jq --argjson new "$NewAttachment" '. + [$new]')
# Merge and update task
local MergedTask=$(echo "$CurrentTask" | jq --argjson attachments "$UpdatedAttachments" '. + {attachments: $attachments}')
local Payload=$(jq -n --argjson task "$MergedTask" '{task: $task}')
_api_call "POST" "/tasks" "$Payload" >/dev/null
echo "✅ File attached: $FileName"
}
# Update an existing comment by ID
task_update_comment() {
local TASK_ID="$1"
local COMMENT_ID="$2"
local NEW_TEXT="$3"
# Get current task
local CurrentTask=$(task_get "$TASK_ID")
local AllComments=$(echo "$CurrentTask" | jq '.comments // []')
# Find and update the specific comment, keep others unchanged
local UpdatedComments=$(echo "$AllComments" | jq --arg cid "$COMMENT_ID" --arg text "$NEW_TEXT" \
'map(if .id == $cid then .text = $text else . end)')
# Merge and update task
local MergedTask=$(echo "$CurrentTask" | jq --argjson comments "$UpdatedComments" '. + {comments: $comments}')
local Payload=$(jq -n --argjson task "$MergedTask" '{task: $task}')
_api_call "POST" "/tasks" "$Payload" >/dev/null
echo "✅ Comment updated"
}
# Export all functions
export -f resolve_project_id
export -f resolve_user_id
export -f sprint_get_current
export -f resolve_sprint_id
export -f task_create
export -f task_get
export -f task_list
export -f task_update
export -f task_delete
export -f task_add_comment
export -f task_update_comment
export -f task_attach_file
export -f _api_call

View File

@ -0,0 +1,267 @@
---
name: mission-control-docs
description: Full CRUD operations for Mission Control Documents. Create, read, update, delete documents. Uses Mission Control API with machine token authentication.
---
# mission-control-docs
**Module Skill** - Complete document management for Mission Control via API.
## Purpose
Provides all operations needed to work with Mission Control documents: create, read, update, delete, list, and search. Uses Mission Control API (not direct Supabase).
## Authentication
Uses **machine token** for server-to-server API calls:
```bash
export MC_MACHINE_TOKEN="50cd5e8fe3f895353f97c9ee64052c0b689b4eedf79259746413734d0a163cf8"
export MC_API_URL="https://mission-control-rho-pink.vercel.app/api"
```
**Architecture:** Skills → API (with machine token) → Database
This follows the API-centric pattern where all business logic lives in API routes.
## Usage
```bash
source ~/.agents/skills/mission-control-docs/lib/docs.sh
# Create document
DOC_ID=$(mc_doc_create \
--title "Article Title" \
--content "$CONTENT" \
--folder "Research/AI & Agents/")
# Get document
mc_doc_get "$DOC_ID"
# List documents
mc_doc_list --folder "Research/"
```
## Functions
### Document CRUD
#### `mc_doc_create([options])`
Create a new document.
**Options:**
- `--title` - Document title (required)
- `--content` - Document content (required)
- `--folder` - Folder path (default: auto-detected)
- `--tags` - Tags as JSON array string (default: auto-detected)
- `--type` - Document type (default: markdown)
- `--description` - Description (optional)
**Returns:** Document ID on success
**Example:**
```bash
DOC_ID=$(mc_doc_create \
--title "API Design Patterns" \
--content "$SUMMARY_CONTENT" \
--folder "Research/Tools & Tech/" \
--tags '["api", "design", "reference"]')
```
**Auto-categorization:** If folder not specified, detects based on content keywords:
- `Research/AI & Agents/` - AI, agent, LLM, Claude, OpenClaw, GPT
- `Research/iOS Development/` - Swift, iOS, Xcode, Apple, SwiftUI
- `Research/Business & Marketing/` - SaaS, business, startup, marketing
- `Research/Tools & Tech/` - tool, library, API, SDK, framework
- `Research/Tutorials/` - tutorial, guide, how-to, walkthrough
- `Research/` - Default
#### `mc_doc_get(DOC_ID)`
Get document by ID.
**Returns:** Document JSON
**Example:**
```bash
DOC=$(mc_doc_get "a1b2c3d4-...")
echo "$DOC" | jq -r '.title'
```
#### `mc_doc_list([filters])`
List documents with optional filters.
**Filters:**
- `--folder` - Filter by folder path
- `--tags` - Filter by tag
- `--search` - Search in title/content
- `--limit` - Max results (default: 50)
- `--json` - Output as JSON
**Example:**
```bash
# List all in folder
mc_doc_list --folder "Research/AI & Agents/"
# Search
mc_doc_list --search "OpenClaw"
# Get JSON for processing
mc_doc_list --folder "Research/" --json
```
#### `mc_doc_update(DOC_ID, [options])`
Update document fields.
**Parameters:**
- `DOC_ID` - Document UUID
- `--title` - New title
- `--content` - New content
- `--folder` - New folder
- `--tags` - New tags (JSON array)
- `--description` - New description
**Example:**
```bash
mc_doc_update "$DOC_ID" \
--title "Updated Title" \
--tags '["research", "ai", "updated"]'
```
#### `mc_doc_delete(DOC_ID)`
Delete document permanently.
**Example:**
```bash
mc_doc_delete "$DOC_ID"
```
### Search & Discovery
#### `mc_doc_search(QUERY, [LIMIT])`
Search documents by title or content.
**Parameters:**
- `QUERY` - Search term
- `LIMIT` - Max results (default: 20)
**Example:**
```bash
# Search for OpenClaw
mc_doc_search "OpenClaw" 10
```
#### `mc_doc_folder_list()`
List all unique folders.
**Example:**
```bash
mc_doc_folder_list
```
### Auto-detection
#### `_detect_folder(CONTENT)`
Auto-detect folder based on content keywords.
**Example:**
```bash
FOLDER=$(_detect_folder "$CONTENT")
echo "Detected folder: $FOLDER"
```
#### `_detect_tags(CONTENT)`
Auto-detect tags based on content keywords.
**Returns:** JSON array of tags
**Example:**
```bash
TAGS=$(_detect_tags "$CONTENT")
echo "Detected tags: $TAGS"
```
## File Structure
```
~/.agents/skills/mission-control-docs/
├── SKILL.md # This documentation
└── lib/
└── docs.sh # All document functions
```
## Integration Example
Complete workflow:
```bash
# Set authentication
export MC_MACHINE_TOKEN="50cd5e8fe3f895353f97c9ee64052c0b689b4eedf79259746413734d0a163cf8"
export MC_API_URL="https://mission-control-rho-pink.vercel.app/api"
source ~/.agents/skills/mission-control-docs/lib/docs.sh
# Research summary from URL
SUMMARY=$(cat << 'EOF'
## Scrapling: Modern Web Scraping Framework
Scrapling is a Python web scraping framework with:
- Adaptive parsing
- Anti-bot bypass
- Full crawling capabilities
**Use Cases:**
- E-commerce monitoring
- Content aggregation
- SEO auditing
**Links:**
- GitHub: https://github.com/D4Vinci/Scrapling
- Docs: https://scrapling.readthedocs.io
EOF
)
# Create document with auto-detection
DOC_ID=$(mc_doc_create \
--title "Scrapling: Web Scraping Framework" \
--content "$SUMMARY" \
--description "Auto-research from URL: https://github.com/D4Vinci/Scrapling")
echo "Document created: $DOC_ID"
# Verify
echo "=== Document Created ==="
mc_doc_get "$DOC_ID" | jq -r '.title, .folder, .tags'
```
## CLI Alternative
For interactive/command-line use, use the Mission Control CLI:
```bash
# Set auth for CLI
export MC_MACHINE_TOKEN="50cd5e8fe3f895353f97c9ee64052c0b689b4eedf79259746413734d0a163cf8"
export MC_API_URL="https://mission-control-rho-pink.vercel.app/api"
cd /Users/mattbruce/Documents/Projects/OpenClaw/Web/mission-control
# List documents
./scripts/mc.sh document list
# Get specific document
./scripts/mc.sh document get $DOC_ID
```
**Note:** The skill library and CLI both use the same API endpoints with machine token authentication.
---
**Note:** This is a low-level module skill. Used by orchestrator skills like `url-research-to-documents-and-tasks`, not directly by users.

View File

@ -0,0 +1,287 @@
#!/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