From 94b89da5335da059c75bccc686be91a923726075 Mon Sep 17 00:00:00 2001 From: m3tm3re Date: Wed, 11 Feb 2026 19:58:06 +0100 Subject: [PATCH] finalize doc-translator skill --- AGENTS.md | 10 ++ scripts/test-skill.sh | 14 +-- skills/doc-translator/SKILL.md | 116 +++++++++--------- .../scripts/list_outline_collections.sh | 32 ----- .../scripts/upload_image_to_outline.sh | 108 +++++++++------- skills/skill-creator/scripts/init_skill.py | 4 +- 6 files changed, 142 insertions(+), 142 deletions(-) delete mode 100755 skills/doc-translator/scripts/list_outline_collections.sh diff --git a/AGENTS.md b/AGENTS.md index a353159..a646a5f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,15 @@ # Opencode Skills Repository +## MANDATORY: Use td for Task Management + +Run td usage --new-session at conversation start (or after /clear). This tells you what to work on next. + +Sessions are automatic (based on terminal/agent context). Optional: +- td session "name" to label the current session +- td session --new to force a new session in the same context + +Use td usage -q after first read. + Configuration repository for Opencode Agent Skills, context files, and agent configurations. Deployed via Nix home-manager to `~/.config/opencode/`. ## Quick Commands diff --git a/scripts/test-skill.sh b/scripts/test-skill.sh index b04984c..e499613 100755 --- a/scripts/test-skill.sh +++ b/scripts/test-skill.sh @@ -8,7 +8,7 @@ # ./scripts/test-skill.sh --run # Launch interactive opencode session # # This script creates a temporary XDG_CONFIG_HOME with symlinks to this -# repository's skill/, context/, command/, and prompts/ directories, +# repository's skills/, context/, command/, and prompts/ directories, # allowing you to test skill changes before deploying via home-manager. set -euo pipefail @@ -72,17 +72,17 @@ list_skills() { validate_skill() { local skill_name="$1" - local skill_path="$REPO_ROOT/skill/$skill_name" + local skill_path="$REPO_ROOT/skills/$skill_name" if [[ ! -d "$skill_path" ]]; then echo -e "${RED}❌ Skill not found: $skill_name${NC}" echo "Available skills:" - ls -1 "$REPO_ROOT/skill/" + ls -1 "$REPO_ROOT/skills/" exit 1 fi echo -e "${YELLOW}Validating skill: $skill_name${NC}" - if python3 "$REPO_ROOT/skill/skill-creator/scripts/quick_validate.py" "$skill_path"; then + if python3 "$REPO_ROOT/skills/skill-creator/scripts/quick_validate.py" "$skill_path"; then echo -e "${GREEN}✅ Skill '$skill_name' is valid${NC}" else echo -e "${RED}❌ Skill '$skill_name' has validation errors${NC}" @@ -95,14 +95,14 @@ validate_all() { echo "" local failed=0 - for skill_dir in "$REPO_ROOT/skill/"*/; do + for skill_dir in "$REPO_ROOT/skills/"*/; do local skill_name=$(basename "$skill_dir") echo -n " $skill_name: " - if python3 "$REPO_ROOT/skill/skill-creator/scripts/quick_validate.py" "$skill_dir" > /dev/null 2>&1; then + if python3 "$REPO_ROOT/skills/skill-creator/scripts/quick_validate.py" "$skill_dir" > /dev/null 2>&1; then echo -e "${GREEN}✅${NC}" else echo -e "${RED}❌${NC}" - python3 "$REPO_ROOT/skill/skill-creator/scripts/quick_validate.py" "$skill_dir" 2>&1 | sed 's/^/ /' + python3 "$REPO_ROOT/skills/skill-creator/scripts/quick_validate.py" "$skill_dir" 2>&1 | sed 's/^/ /' ((failed++)) || true fi done diff --git a/skills/doc-translator/SKILL.md b/skills/doc-translator/SKILL.md index 2464c63..9044788 100644 --- a/skills/doc-translator/SKILL.md +++ b/skills/doc-translator/SKILL.md @@ -72,76 +72,72 @@ If an image download fails, log it and continue. Use a placeholder in the final ### 4. Upload Images to Outline -**IMPORTANT:** Always use Outline MCP tools for all Outline operations. If Outline tools throw errors: -1. Load the outline skill first: `skill name=outline` -2. Retry with `skill_mcp` tool for outline operations -3. Only fallback to direct API calls via `bash` after exhausting MCP options - -mcp-outline cannot create attachments. Use direct API calls via `bash` for image uploads only. - -**Required env:** `OUTLINE_API_KEY` (read from /run/agenix/outline-key) - -For each downloaded image: +MCP-outline does not support attachment creation. Use the bundled script for image uploads: ```bash -#!/usr/bin/env bash -set -euo pipefail +# Upload with optional document association +bash scripts/upload_image_to_outline.sh "/tmp/doc-images/screenshot.png" "$DOCUMENT_ID" -IMAGE_PATH="/tmp/doc-images/screenshot.png" -IMAGE_NAME="$(basename "$IMAGE_PATH")" -CONTENT_TYPE="image/png" # Detect from extension: png->image/png, jpg/jpeg->image/jpeg, gif->image/gif, svg->image/svg+xml, webp->image/webp - -# 1. Get file size (cross-platform) -FILESIZE=$(stat -f%z "$IMAGE_PATH" 2>/dev/null || stat -c%s "$IMAGE_PATH") - -# 2. Create attachment record -RESPONSE=$(curl -s -X POST "https://wiki.az-gruppe.com/api/attachments.create" \ - -H "Authorization: Bearer $OUTLINE_API_KEY" \ - -H "Content-Type: application/json" \ - -d "{ - \"name\": \"$IMAGE_NAME\", - \"contentType\": \"$CONTENT_TYPE\", - \"size\": $FILESIZE - }") - -# 3. Extract URLs from response -UPLOAD_URL=$(echo "$RESPONSE" | jq -r '.data.uploadUrl') -ATTACHMENT_URL=$(echo "$RESPONSE" | jq -r '.data.attachment.url') - -# 4. Check for errors -if [ "$UPLOAD_URL" = "null" ] || [ -z "$UPLOAD_URL" ]; then - echo "ERROR: Failed to create attachment. Response: $RESPONSE" >&2 - exit 1 -fi - -# 5. Upload binary to signed URL -curl -s -X PUT "$UPLOAD_URL" \ - -H "Content-Type: $CONTENT_TYPE" \ - --data-binary "@$IMAGE_PATH" - -# 6. Output the attachment URL for use in markdown -echo "$ATTACHMENT_URL" +# Upload without document (attach later) +bash scripts/upload_image_to_outline.sh "/tmp/doc-images/screenshot.png" ``` -Replace image references in the translated markdown: +The script handles API key loading from `/run/agenix/outline-key`, content-type detection, the two-step presigned POST flow, and retries. Output is JSON: `{"success": true, "attachment_url": "https://..."}`. + +Replace image references in the translated markdown with the returned `attachment_url`: ```markdown ![description](ATTACHMENT_URL) ``` -**Content-Type detection by extension:** - -| Extension | Content-Type | -|-----------|-------------| -| `.png` | `image/png` | -| `.jpg`, `.jpeg` | `image/jpeg` | -| `.gif` | `image/gif` | -| `.svg` | `image/svg+xml` | -| `.webp` | `image/webp` | +For all other Outline operations (documents, collections, search), use MCP tools (`Outline_*`). ### 5. Translate with TEEM Format Translate the entire document into each target language. Apply TEEM format to UI elements. +#### Address Form (CRITICAL) + +**Always use the informal "you" form** in ALL target languages: +- **German**: Use **"Du"** (informal), NEVER "Sie" (formal) +- **Czech**: Use **"ty"** (informal), NEVER "vy" (formal) +- This applies to all translations — documentation should feel approachable and direct + +#### Infobox / Callout Formatting + +Source documentation often uses admonitions, callouts, or info boxes (e.g., GitHub-style `> [!NOTE]`, Docusaurus `:::note`, or custom HTML boxes). **Convert ALL such elements** to Outline's callout syntax: + +```markdown +:::tip +Tip or best practice content here. + +::: + +:::info +Informational content here. + +::: + +:::warning +Warning or caution content here. + +::: + +:::success +Success message or positive outcome here. + +::: +``` + +**Mapping rules** (source → Outline): +| Source pattern | Outline syntax | +|---|---| +| Note, Info, Information | `:::info` | +| Tip, Hint, Best Practice | `:::tip` | +| Warning, Caution, Danger, Important | `:::warning` | +| Success, Done, Check | `:::success` | + +**CRITICAL formatting**: The closing `:::` MUST be on its own line with an empty line before it. Content goes directly after the opening line. + #### TEEM Rules **Format:** `**English UI Term** (Translation)` @@ -217,7 +213,7 @@ Use mcp-outline tools to publish: |-------|--------| | URL fetch fails | Use `question` to ask for alternative URL or manual paste | | Image download fails | Continue with placeholder, note in completion report | -| Outline API error (attachments) | Save markdown to `/tmp/doc-translator-backup-TIMESTAMP.md`, report error | +| Outline API error (attachments) | Script retries 3x with backoff; on final failure save markdown to `/tmp/doc-translator-backup-TIMESTAMP.md`, report error | | Outline API error (document) | Save markdown to `/tmp/doc-translator-backup-TIMESTAMP.md`, report error | | Ambiguous UI term | Use `question` to ask user for correct translation | | Large document (>5000 words) | Ask user if splitting into multiple docs is preferred | @@ -254,9 +250,9 @@ Items Needing Review: ## Environment Variables -| Variable | Purpose | -|----------|---------| -| `OUTLINE_API_KEY` | Bearer token for wiki.az-gruppe.com API | +| Variable | Purpose | Source | +|----------|---------|--------| +| `OUTLINE_API_KEY` | Bearer token for wiki.az-gruppe.com API | Auto-loaded from `/run/agenix/outline-key` by upload script | ## Integration with Other Skills diff --git a/skills/doc-translator/scripts/list_outline_collections.sh b/skills/doc-translator/scripts/list_outline_collections.sh deleted file mode 100755 index 03dabca..0000000 --- a/skills/doc-translator/scripts/list_outline_collections.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -# List all collections available in Outline -# -# Usage: -# list_outline_collections.sh -# -# Environment: -# OUTLINE_API_KEY - Bearer token for wiki.az-gruppe.com API -# -# Output (JSON): -# [{"id": "...", "name": "...", "url": "..."}, ...] - -set -euo pipefail - -# List collections -RESPONSE=$(curl -s -X POST "https://wiki.az-gruppe.com/api/collections.list" \ - -H "Authorization: Bearer $OUTLINE_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{}') - -# Check for errors -if echo "$RESPONSE" | jq -e '.error' > /dev/null 2>&1; then - ERROR_MSG=$(echo "$RESPONSE" | jq -r '.message // "Failed to list collections"') - echo "{\"success\": false, \"error\": \"$ERROR_MSG\", \"response\": \"$RESPONSE\"}" >&2 - exit 1 -fi - -# Extract collection list -COLLECTIONS=$(echo "$RESPONSE" | jq -r '.data[] | {id: .id, name: .name, url: .url}') - -# Output as JSON array -echo "$COLLECTIONS" | jq -s '.' diff --git a/skills/doc-translator/scripts/upload_image_to_outline.sh b/skills/doc-translator/scripts/upload_image_to_outline.sh index 3e5f204..e7d392b 100755 --- a/skills/doc-translator/scripts/upload_image_to_outline.sh +++ b/skills/doc-translator/scripts/upload_image_to_outline.sh @@ -1,26 +1,40 @@ #!/usr/bin/env bash -# Upload an image to an Outline document via signed URL +# Upload an image to Outline via presigned POST (two-step flow) # # Usage: -# upload_image_to_outline.sh +# upload_image_to_outline.sh [document_id] # # Environment: # OUTLINE_API_KEY - Bearer token for wiki.az-gruppe.com API +# Auto-loaded from /run/agenix/outline-key if not set # -# Output (JSON): -# {"success": true, "attachment_url": "https://...", "document_id": "..."} -# OR +# Output (JSON to stdout): +# {"success": true, "attachment_url": "https://..."} +# Error (JSON to stderr): # {"success": false, "error": "error message"} set -euo pipefail -if [ $# -ne 2 ]; then - echo '{"success": false, "error": "Usage: upload_image_to_outline.sh "}' >&2 +MAX_RETRIES=3 +RETRY_DELAY=2 + +if [ $# -lt 1 ] || [ $# -gt 2 ]; then + echo '{"success": false, "error": "Usage: upload_image_to_outline.sh [document_id]"}' >&2 exit 1 fi IMAGE_PATH="$1" -DOCUMENT_ID="$2" +DOCUMENT_ID="${2:-}" + +if [ -z "${OUTLINE_API_KEY:-}" ]; then + if [ -f /run/agenix/outline-key ]; then + OUTLINE_API_KEY=$(cat /run/agenix/outline-key) + export OUTLINE_API_KEY + else + echo '{"success": false, "error": "OUTLINE_API_KEY not set and /run/agenix/outline-key not found"}' >&2 + exit 1 + fi +fi # Check if file exists if [ ! -f "$IMAGE_PATH" ]; then @@ -31,60 +45,72 @@ fi # Extract image name and extension IMAGE_NAME="$(basename "$IMAGE_PATH")" EXTENSION="${IMAGE_NAME##*.}" -IMAGE_NAME_BASE="${IMAGE_NAME%.*}" # Detect content type by extension -case "$EXTENSION" in - png) CONTENT_TYPE="image/png" ;; - jpg|jpeg) CONTENT_TYPE="image/jpeg" ;; - gif) CONTENT_TYPE="image/gif" ;; - svg) CONTENT_TYPE="image/svg+xml" ;; - webp) CONTENT_TYPE="image/webp" ;; - *) CONTENT_TYPE="application/octet-stream" ;; +case "${EXTENSION,,}" in + png) CONTENT_TYPE="image/png" ;; + jpg|jpeg) CONTENT_TYPE="image/jpeg" ;; + gif) CONTENT_TYPE="image/gif" ;; + svg) CONTENT_TYPE="image/svg+xml" ;; + webp) CONTENT_TYPE="image/webp" ;; + *) CONTENT_TYPE="application/octet-stream" ;; esac -# Get file size (cross-platform: macOS uses stat -f%z, Linux uses stat -c%s) -FILESIZE=$(stat -f%z "$IMAGE_PATH" 2>/dev/null || stat -c%s "$IMAGE_PATH" 2>/dev/null) +FILESIZE=$(stat -c%s "$IMAGE_PATH" 2>/dev/null || stat -f%z "$IMAGE_PATH" 2>/dev/null) if [ -z "$FILESIZE" ]; then echo "{\"success\": false, \"error\": \"Failed to get file size for: $IMAGE_PATH\"}" >&2 exit 1 fi -# Create attachment record +REQUEST_BODY=$(jq -n \ + --arg name "$IMAGE_NAME" \ + --arg contentType "$CONTENT_TYPE" \ + --argjson size "$FILESIZE" \ + --arg documentId "$DOCUMENT_ID" \ + 'if $documentId == "" then + {name: $name, contentType: $contentType, size: $size} + else + {name: $name, contentType: $contentType, size: $size, documentId: $documentId} + end') + +# Step 1: Create attachment record RESPONSE=$(curl -s -X POST "https://wiki.az-gruppe.com/api/attachments.create" \ -H "Authorization: Bearer $OUTLINE_API_KEY" \ -H "Content-Type: application/json" \ - -d "{ - \"name\": \"$IMAGE_NAME\", - \"contentType\": \"$CONTENT_TYPE\", - \"size\": $FILESIZE, - \"documentId\": \"$DOCUMENT_ID\" - }") + -d "$REQUEST_BODY") -# Extract URLs from response UPLOAD_URL=$(echo "$RESPONSE" | jq -r '.data.uploadUrl // empty') ATTACHMENT_URL=$(echo "$RESPONSE" | jq -r '.data.attachment.url // empty') -# Check for errors -if [ -z "$UPLOAD_URL" ] || [ "$UPLOAD_URL" = "null" ]; then +if [ -z "$UPLOAD_URL" ]; then ERROR_MSG=$(echo "$RESPONSE" | jq -r '.message // "Failed to create attachment"') - echo "{\"success\": false, \"error\": \"$ERROR_MSG\", \"response\": \"$RESPONSE\"}" >&2 + echo "{\"success\": false, \"error\": \"$ERROR_MSG\", \"response\": $(echo "$RESPONSE" | jq -c .)}" >&2 exit 1 fi -# Upload binary to signed URL -UPLOAD_RESPONSE=$(curl -s -w "%{http_code}" -X POST "$UPLOAD_URL" \ - -H "Content-Type: $CONTENT_TYPE" \ - --data-binary "@$IMAGE_PATH") +FORM_ARGS=() +while IFS= read -r line; do + key=$(echo "$line" | jq -r '.key') + value=$(echo "$line" | jq -r '.value') + FORM_ARGS+=(-F "$key=$value") +done < <(echo "$RESPONSE" | jq -c '.data.form | to_entries[]') -# Extract HTTP status code (last 3 characters) -HTTP_CODE="${UPLOAD_RESPONSE: -3}" +# Step 2: Upload binary to presigned URL with retry +for attempt in $(seq 1 "$MAX_RETRIES"); do + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST "$UPLOAD_URL" \ + "${FORM_ARGS[@]}" \ + -F "file=@$IMAGE_PATH") -if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ]; then - echo "{\"success\": false, \"error\": \"Upload failed with HTTP $HTTP_CODE\", \"upload_url\": \"$UPLOAD_URL\"}" >&2 - exit 1 -fi + if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "204" ]; then + echo "{\"success\": true, \"attachment_url\": \"$ATTACHMENT_URL\"}" + exit 0 + fi -# Output success result -echo "{\"success\": true, \"attachment_url\": \"$ATTACHMENT_URL\", \"document_id\": \"$DOCUMENT_ID\"}" + if [ "$attempt" -lt "$MAX_RETRIES" ]; then + sleep "$((RETRY_DELAY * attempt))" + fi +done + +echo "{\"success\": false, \"error\": \"Upload failed after $MAX_RETRIES attempts (last HTTP $HTTP_CODE)\"}" >&2 +exit 1 diff --git a/skills/skill-creator/scripts/init_skill.py b/skills/skill-creator/scripts/init_skill.py index e3c1abd..c5204ab 100755 --- a/skills/skill-creator/scripts/init_skill.py +++ b/skills/skill-creator/scripts/init_skill.py @@ -6,8 +6,8 @@ Usage: init_skill.py --path Examples: - init_skill.py my-new-skill --path ~/.config/opencode/skill - init_skill.py my-api-helper --path .opencode/skill + init_skill.py my-new-skill --path ~/.config/opencode/skills + init_skill.py my-api-helper --path .opencode/skills init_skill.py custom-skill --path /custom/location """