Skill: jilycn/slack-extended (version 0.1.1)

--- name: slack-extended description: Upload files, manage canvases, and manage bookmarks in Slack. Use when you need to share files, create/edit canvases, or add/organize link bookmarks in Slack channels. Complements the core slack skill which handles messages, reactions, and pins. metadata: { "openclaw": { "emoji": "📎", "requires": { "config": ["channels.slack"] }, "credentials": { "source": "~/.openclaw/openclaw.json", "keys": ["channels.slack.botToken"], "scopes": ["files:write", "canvases:write", "bookmarks:write", "bookmarks:read"] } } } --- # Slack Extended Extends the core `slack` skill with file uploads and canvas management. Uses Python scripts that call the Slack API directly with the bot token from `~/.openclaw/openclaw.json`. **Requires OAuth scopes:** `files:write`, `canvases:write` (add at api.slack.com if missing). ## File Upload Upload a local file to a Slack channel: ```bash python3 /mnt/openclaw/skills/slack-extended/scripts/slack_file_upload.py \ --channel C123ABC \ --file /path/to/file.png \ --title "Q4 Report" \ --message "Here's the latest report" ``` **Arguments:** - `--channel` (required): Channel ID to share the file in - `--file` (required): Path to the local file - `--title`: Display title (defaults to filename) - `--message`: Comment posted with the file Returns JSON with `file_id`, `permalink`, and `channel`. **Common patterns:** - Share a generated chart: `--file /tmp/chart.png --title "Performance Chart"` - Share a text file: `--file ./notes.txt --title "Meeting Notes"` - Share with context: `--message "Backtest results for GEM v2" --file results.csv` ## Canvas Operations Manage Slack canvases (collaborative documents): ### Create a canvas ```bash python3 /mnt/openclaw/skills/slack-extended/scripts/slack_canvas.py create \ --title "Sprint Notes" \ --markdown "## Goals\n- Ship feature X\n- Fix bug Y" ``` ### Edit a canvas Append content: ```bash python3 /mnt/openclaw/skills/slack-extended/scripts/slack_canvas.py edit \ --canvas-id F07ABCD1234 \ --operation insert_at_end \ --markdown "## Update\nNew section added" ``` Replace a section: ```bash python3 /mnt/openclaw/skills/slack-extended/scripts/slack_canvas.py edit \ --canvas-id F07ABCD1234 \ --section-id temp:C:abc123 \ --operation replace \ --markdown "## Revised Section\nUpdated content" ``` **Operations:** `insert_at_start`, `insert_at_end`, `insert_after`, `replace`, `delete` ### Look up sections ```bash python3 /mnt/openclaw/skills/slack-extended/scripts/slack_canvas.py sections \ --canvas-id F07ABCD1234 ``` ### Delete a canvas ```bash python3 /mnt/openclaw/skills/slack-extended/scripts/slack_canvas.py delete \ --canvas-id F07ABCD1234 ``` ### Set access ```bash python3 /mnt/openclaw/skills/slack-extended/scripts/slack_canvas.py access \ --canvas-id F07ABCD1234 \ --channel C123ABC \ --level edit ``` ## Canvas Markdown Canvases support: bold, italic, strikethrough, headings (h1-h3), bulleted/ordered lists, checklists, code blocks, code spans, links, tables (max 300 cells), blockquotes, dividers, emojis. **Mentions:** `![](@USER_ID)` for users, `![](#CHANNEL_ID)` for channels. ## Bookmarks Manage link bookmarks in the bookmark bar at the top of Slack channels. **Limitation:** Slack API only supports **link** bookmarks. Folders are a UI-only feature and cannot be created via the API. **Requires OAuth scopes:** `bookmarks:write`, `bookmarks:read` ### List bookmarks ```bash python3 /mnt/openclaw/skills/slack-extended/scripts/slack_bookmark.py list \ --channel C123ABC ``` ### Add a link bookmark ```bash python3 /mnt/openclaw/skills/slack-extended/scripts/slack_bookmark.py add \ --channel C123ABC \ --title "Design Docs" \ --link "https://example.com" \ --emoji ":link:" ``` ### Edit a bookmark ```bash python3 /mnt/openclaw/skills/slack-extended/scripts/slack_bookmark.py edit \ --channel C123ABC \ --bookmark-id Bk123 \ --title "New Title" ``` ### Remove a bookmark ```bash python3 /mnt/openclaw/skills/slack-extended/scripts/slack_bookmark.py remove \ --channel C123ABC \ --bookmark-id Bk123 ``` ## Troubleshooting - **`missing_scope` error**: Add the required scope (`files:write` or `canvases:write`) at api.slack.com, then reinstall the app to the workspace. - **`channel_not_found`**: Use the channel ID (e.g. `C07ABC123`), not the channel name. - **`not_authed`**: Bot token may have changed. Check `~/.openclaw/openclaw.json`. - **Canvas edit fails**: Look up sections first to get valid `section_id` values. - **`missing_scope` for bookmarks**: Add `bookmarks:write` and `bookmarks:read` at api.slack.com, then reinstall. - **Folders not supported**: Slack API does not support creating folders — only link bookmarks. Folders can only be created manually in the Slack UI.

path: scripts/slack_bookmark.py
content: #!/usr/bin/env python3
"""Manage Slack channel bookmarks (add, list, edit, remove).

Note: Slack API only supports link bookmarks. Folders are UI-only and cannot be
created via the API.

Usage:
    python3 slack_bookmark.py add --channel C123ABC --title "Design Docs" --link "https://example.com"
    python3 slack_bookmark.py list --channel C123ABC
    python3 slack_bookmark.py edit --channel C123ABC --bookmark-id Bk123 --title "New Title"
    python3 slack_bookmark.py remove --channel C123ABC --bookmark-id Bk123
"""

import argparse
import json
import os
import sys
import urllib.request
import urllib.parse
import urllib.error

CONFIG_PATH = os.path.expanduser("~/.openclaw/openclaw.json")


def get_bot_token():
    with open(CONFIG_PATH) as f:
        config = json.load(f)
    [REDACTED_SECRET]"channels", {}).get("slack", {}).get("botToken")
    if not [REDACTED_SECRET]"Error: No botToken found in", CONFIG_PATH, file=sys.stderr)
        sys.exit(1)
    return token


def slack_api(token, method, params):
    """POST to Slack API with application/x-www-form-urlencoded."""
    url = f"https://slack.com/api/{method}"
    headers = {"Authorization": f"Bearer {token}"}
    body = urllib.parse.urlencode(params).encode()
    req = urllib.request.Request(url, data=body, headers=headers)
    try:
        with urllib.request.urlopen(req) as resp:
            return json.loads(resp.read())
    except urllib.error.HTTPError as e:
        error_body = e.read().decode()
        print(f"Error: HTTP {e.code} from {method}: {error_body}", file=sys.stderr)
        sys.exit(1)


def cmd_add(args):
    [REDACTED_SECRET])
    if not args.link:
        print("Error: --link is required (Slack API only supports link bookmarks, not folders)", file=sys.stderr)
        sys.exit(1)
    params = {
        "channel_id": args.channel,
        "title": args.title,
        "type": "link",
        "link": args.link,
    }
    if args.emoji:
        params["emoji"] = args.emoji

    resp = slack_api(token, "bookmarks.add", params)
    if not resp.get("ok"):
        print(f"Error: bookmarks.add failed: {resp.get('error', 'unknown')}", file=sys.stderr)
        sys.exit(1)

    bookmark = resp.get("bookmark", {})
    print(json.dumps({
        "ok": True,
        "bookmark_id": bookmark.get("id"),
        "title": bookmark.get("title"),
        "type": bookmark.get("type"),
        "link": bookmark.get("link"),
        "channel": args.channel,
    }, indent=2))


def cmd_list(args):
    [REDACTED_SECRET])
    resp = slack_api(token, "bookmarks.list", {"channel_id": args.channel})
    if not resp.get("ok"):
        print(f"Error: bookmarks.list failed: {resp.get('error', 'unknown')}", file=sys.stderr)
        sys.exit(1)

    bookmarks = resp.get("bookmarks", [])
    result = []
    for b in bookmarks:
        entry = {
            "id": b.get("id"),
            "title": b.get("title"),
            "type": b.get("type"),
            "link": b.get("link", ""),
        }
        if b.get("emoji"):
            entry["emoji"] = b["emoji"]
        result.append(entry)
    print(json.dumps({"ok": True, "bookmarks": result}, indent=2))


def cmd_edit(args):
    [REDACTED_SECRET])
    params = {
        "channel_id": args.channel,
        "bookmark_id": args.bookmark_id,
    }
    if args.title:
        params["title"] = args.title
    if args.link:
        params["link"] = args.link
    if args.emoji:
        params["emoji"] = args.emoji

    resp = slack_api(token, "bookmarks.edit", params)
    if not resp.get("ok"):
        print(f"Error: bookmarks.edit failed: {resp.get('error', 'unknown')}", file=sys.stderr)
        sys.exit(1)

    bookmark = resp.get("bookmark", {})
    print(json.dumps({
        "ok": True,
        "bookmark_id": bookmark.get("id"),
        "title": bookmark.get("title"),
    }, indent=2))


def cmd_remove(args):
    [REDACTED_SECRET])
    resp = slack_api(token, "bookmarks.remove", {
        "channel_id": args.channel,
        "bookmark_id": args.bookmark_id,
    })
    if not resp.get("ok"):
        print(f"Error: bookmarks.remove failed: {resp.get('error', 'unknown')}", file=sys.stderr)
        sys.exit(1)

    print(json.dumps({"ok": True, "removed": args.bookmark_id}, indent=2))


def main():
    parser = argparse.ArgumentParser(description="Manage Slack channel bookmarks")
    sub = parser.add_subparsers(dest="command", required=True)

    # add
    p_add = sub.add_parser("add", help="Add a link bookmark")
    p_add.add_argument("--channel", required=True, help="Channel ID")
    p_add.add_argument("--title", required=True, help="Bookmark title")
    p_add.add_argument("--link", required=True, help="URL for the bookmark")
    p_add.add_argument("--emoji", help="Emoji icon (e.g. :link:)")

    # list
    p_list = sub.add_parser("list", help="List bookmarks in a channel")
    p_list.add_argument("--channel", required=True, help="Channel ID")

    # edit
    p_edit = sub.add_parser("edit", help="Edit a bookmark")
    p_edit.add_argument("--channel", required=True, help="Channel ID")
    p_edit.add_argument("--bookmark-id", required=True, help="Bookmark ID to edit")
    p_edit.add_argument("--title", help="New title")
    p_edit.add_argument("--link", help="New URL")
    p_edit.add_argument("--emoji", help="New emoji")

    # remove
    p_remove = sub.add_parser("remove", help="Remove a bookmark")
    p_remove.add_argument("--channel", required=True, help="Channel ID")
    p_remove.add_argument("--bookmark-id", required=True, help="Bookmark ID to remove")

    args = parser.parse_args()
    {"add": cmd_add, "list": cmd_list, "edit": cmd_edit, "remove": cmd_remove}[args.command](args)


if __name__ == "__main__":
    main()


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