@sulala/agent-os 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/data/agents/briefing_agent.json +12 -0
- package/data/agents/dev_agent.json +10 -0
- package/data/agents/manager_agent.json +11 -0
- package/data/agents/media_agent.json +10 -0
- package/data/agents/personal_agent.json +11 -0
- package/data/agents/research_agent.json +10 -0
- package/data/agents/social_media_agent.json +10 -0
- package/data/agents/writer_agent.json +10 -0
- package/data/skills/bluesky/SKILL.md +63 -0
- package/data/skills/bluesky/config.schema.json +17 -0
- package/data/skills/bluesky/scripts/post.sh +48 -0
- package/data/skills/bluesky/scripts/timeline.sh +37 -0
- package/data/skills/date/SKILL.md +53 -0
- package/data/skills/fetch/SKILL.md +42 -0
- package/data/skills/file-search/SKILL.md +122 -0
- package/data/skills/file-stats/SKILL.md +68 -0
- package/data/skills/git/SKILL.md +60 -0
- package/data/skills/gmail/SKILL.md +55 -0
- package/data/skills/gmail/references/send-email.md +54 -0
- package/data/skills/gmail/scripts/send_email.py +94 -0
- package/data/skills/hash/SKILL.md +56 -0
- package/data/skills/jq/SKILL.md +66 -0
- package/data/skills/markdown-to-html/SKILL.md +47 -0
- package/data/skills/memory/SKILL.md +64 -0
- package/data/skills/qr-code/SKILL.md +65 -0
- package/data/skills/rss/SKILL.md +40 -0
- package/data/skills/sulala-portal/SKILL.md +92 -0
- package/data/skills/translate/SKILL.md +52 -0
- package/data/skills/weather/SKILL.md +59 -0
- package/data/skills/web-search/SKILL.md +55 -0
- package/data/skills/web-search/config.schema.json +12 -0
- package/data/skills/web-search/scripts/search.sh +21 -0
- package/data/skills/webhook/SKILL.md +101 -0
- package/data/skills/webhook/config.schema.json +11 -0
- package/data/skills/webhook/scripts/post.sh +13 -0
- package/data/skills/youtube/SKILL.md +91 -0
- package/data/skills/youtube/config.schema.json +11 -0
- package/data/skills/youtube/package.json +8 -0
- package/data/skills/youtube/references/youtube-upload.md +65 -0
- package/data/skills/youtube/requirements.txt +3 -0
- package/data/skills/youtube/scripts/youtube_upload.js +200 -0
- package/data/skills/youtube/scripts/youtube_upload.py +125 -0
- package/data/templates/BOOT.md +11 -0
- package/data/templates/BOOTSTRAP.md +62 -0
- package/data/templates/HEARTBEAT.md +12 -0
- package/data/templates/IDENTITY.dev.md +62 -0
- package/data/templates/IDENTITY.md +60 -0
- package/data/templates/SYSTEM.dev.md +82 -0
- package/data/templates/SYSTEM.md +65 -0
- package/data/templates/TOOLS.dev.md +24 -0
- package/data/templates/TOOLS.md +47 -0
- package/data/templates/USER.dev.md +18 -0
- package/data/templates/USER.md +23 -0
- package/dist/cli.js +22 -13
- package/dist/index.js +14 -5
- package/package.json +3 -2
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: weather
|
|
3
|
+
description: Get current weather and forecasts (no API key required). Use when the user asks for weather, temperature, or forecast in a city or location.
|
|
4
|
+
homepage: https://wttr.in/:help
|
|
5
|
+
metadata:
|
|
6
|
+
clawdbot:
|
|
7
|
+
emoji: "🌤️"
|
|
8
|
+
requires:
|
|
9
|
+
bins:
|
|
10
|
+
- curl
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Weather
|
|
14
|
+
|
|
15
|
+
Two free services, no API keys needed. Use the **exec** tool (no `skill_id` needed for curl, or use `skill_id: "weather"` if running from skill dir). Commands below use **curl**.
|
|
16
|
+
|
|
17
|
+
## wttr.in (primary)
|
|
18
|
+
|
|
19
|
+
Quick one-liner:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
curl -s "wttr.in/London?format=3"
|
|
23
|
+
# Output: London: ⛅️ +8°C
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Compact format:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
curl -s "wttr.in/London?format=%l:+%c+%t+%h+%w"
|
|
30
|
+
# Output: London: ⛅️ +8°C 71% ↙5km/h
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Full forecast:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
curl -s "wttr.in/London?T"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Format codes: `%c` condition · `%t` temp · `%h` humidity · `%w` wind · `%l` location · `%m` moon
|
|
40
|
+
|
|
41
|
+
Tips:
|
|
42
|
+
|
|
43
|
+
- URL-encode spaces: `wttr.in/New+York`
|
|
44
|
+
- Airport codes: `wttr.in/JFK`
|
|
45
|
+
- Units: `?m` (metric) `?u` (USCS)
|
|
46
|
+
- Today only: `?1` · Current only: `?0`
|
|
47
|
+
- PNG: `curl -s "wttr.in/Berlin.png" -o /tmp/weather.png`
|
|
48
|
+
|
|
49
|
+
## Open-Meteo (fallback, JSON)
|
|
50
|
+
|
|
51
|
+
Free, no key, good for programmatic use:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
curl -s "https://api.open-meteo.com/v1/forecast?latitude=51.5&longitude=-0.12¤t_weather=true"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Find coordinates for a city, then query. Returns JSON with temp, windspeed, weathercode.
|
|
58
|
+
|
|
59
|
+
Docs: https://open-meteo.com/en/docs
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web-search
|
|
3
|
+
description: Search the web and get titles, snippets, and URLs. Use when the user says "search the web for …", "find recent info on …", "look up …", "what's the latest on …".
|
|
4
|
+
credentials:
|
|
5
|
+
- SERPER_API_KEY
|
|
6
|
+
metadata:
|
|
7
|
+
clawdbot:
|
|
8
|
+
emoji: "🔍"
|
|
9
|
+
requires:
|
|
10
|
+
bins:
|
|
11
|
+
- curl
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Web search
|
|
15
|
+
|
|
16
|
+
Search the web via the **Serper** API (Google search). Results are returned as JSON (organic results with title, link, snippet). Use the **exec** tool with **skill_id: "web-search"** so `SERPER_API_KEY` is injected from skill config.
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
1. Add the **web-search** skill to the agent.
|
|
21
|
+
2. Set **SERPER_API_KEY** in Skills → web-search → Setup (see **Setup** below).
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
Get an API key from [serper.dev](https://serper.dev). Free tier: 2,500 searches/month.
|
|
26
|
+
|
|
27
|
+
1. Sign up at https://serper.dev
|
|
28
|
+
2. Copy your API key from the dashboard.
|
|
29
|
+
3. In the dashboard: **Skills** → **web-search** → ⋮ → **Setup** → paste the key as **Serper API key** → Save.
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
Run the script with a single argument (the search query). From the skill directory:
|
|
34
|
+
|
|
35
|
+
- **skill_id:** `web-search`
|
|
36
|
+
- **command:** `./scripts/search.sh "what is the capital of France"`
|
|
37
|
+
|
|
38
|
+
The script POSTs to Serper and returns JSON. Example response shape (abbreviated):
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"organic": [
|
|
43
|
+
{ "title": "...", "link": "https://...", "snippet": "..." },
|
|
44
|
+
...
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Summarize the `organic` array (title, link, snippet) for the user. For "latest" or news-style queries, use the same command; results are ordered by relevance/recency.
|
|
50
|
+
|
|
51
|
+
## Tips
|
|
52
|
+
|
|
53
|
+
- Quote the query in the command so multi-word searches work: `./scripts/search.sh "best practices for TypeScript 2024"`.
|
|
54
|
+
- If the response is large, the agent can summarize the first few results or filter by relevance.
|
|
55
|
+
- Serper also supports images, news, places; the default script uses `/search`. For news-only, the API endpoint is `https://google.serper.dev/news` with the same body and key.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"properties": {
|
|
4
|
+
"SERPER_API_KEY": {
|
|
5
|
+
"type": "string",
|
|
6
|
+
"format": "password",
|
|
7
|
+
"title": "Serper API key",
|
|
8
|
+
"description": "API key from serper.dev. Free tier: 2500 searches/month. Set in skill config."
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"required": ["SERPER_API_KEY"]
|
|
12
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Search the web via Serper (Google) API. SERPER_API_KEY from skill config (injected as env by exec).
|
|
3
|
+
# Usage: search.sh "your search query"
|
|
4
|
+
if [ -z "$SERPER_API_KEY" ]; then
|
|
5
|
+
echo "SERPER_API_KEY is not set. Configure it in the web-search skill settings (Skills → web-search → Setup)." >&2
|
|
6
|
+
exit 1
|
|
7
|
+
fi
|
|
8
|
+
if [ -z "$1" ]; then
|
|
9
|
+
echo "Usage: search.sh \"search query\"" >&2
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
# Build JSON body safely (jq preferred; fallback to simple printf)
|
|
13
|
+
if command -v jq >/dev/null 2>&1; then
|
|
14
|
+
body=$(jq -n --arg q "$1" '{q: $q}')
|
|
15
|
+
else
|
|
16
|
+
body="{\"q\": \"$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g')\"}"
|
|
17
|
+
fi
|
|
18
|
+
curl -sS -X POST "https://google.serper.dev/search" \
|
|
19
|
+
-H "X-API-KEY: $SERPER_API_KEY" \
|
|
20
|
+
-H "Content-Type: application/json" \
|
|
21
|
+
-d "$body"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: webhook
|
|
3
|
+
description: Send a notification or message to a configurable webhook URL (e.g. Slack, Discord). Use when the user says "notify me when …", "send this to Slack", "post a reminder", "send to webhook", "notify …".
|
|
4
|
+
credentials:
|
|
5
|
+
- WEBHOOK_URL
|
|
6
|
+
metadata:
|
|
7
|
+
clawdbot:
|
|
8
|
+
emoji: "🔔"
|
|
9
|
+
requires:
|
|
10
|
+
bins:
|
|
11
|
+
- curl
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Webhook / Notify
|
|
15
|
+
|
|
16
|
+
POST a JSON body to a URL configured in the skill. The **webhook URL** is stored in skill config (Skills → webhook → Configure) so the agent never sees it. Use the **exec** tool with **skill_id: "webhook"** so `WEBHOOK_URL` is injected from config.
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
1. Add the **webhook** skill to the agent.
|
|
21
|
+
2. Configure **WEBHOOK_URL** in the skill (Skills → webhook → ⋮ → Setup). See **Setup** below for where to get the URL.
|
|
22
|
+
|
|
23
|
+
## Setup
|
|
24
|
+
|
|
25
|
+
Set **WEBHOOK_URL** in Skills → webhook → Setup. Where to get it:
|
|
26
|
+
|
|
27
|
+
### Slack — Incoming Webhook URL
|
|
28
|
+
|
|
29
|
+
1. Open [Slack API apps](https://api.slack.com/apps) and sign in.
|
|
30
|
+
2. **Create an app** → **From scratch** (or use an existing app).
|
|
31
|
+
3. Open the app → **Incoming Webhooks** in the left sidebar → turn **Activate Incoming Webhooks** **On**.
|
|
32
|
+
4. At the bottom, click **Add New Webhook to Workspace**.
|
|
33
|
+
5. Pick the **channel** where messages should go (e.g. `#general` or `#notifications`) → **Allow**.
|
|
34
|
+
6. Copy the **Webhook URL** (looks like `https://hooks.slack.com/services/T00000000/B00000000/xxxxxxxxxxxxxxxxxxxxxxxx`).
|
|
35
|
+
|
|
36
|
+
That URL is your **WEBHOOK_URL** for Slack. Each “Add New Webhook to Workspace” creates one URL; you can add more for other channels.
|
|
37
|
+
|
|
38
|
+
Docs: [Slack Incoming Webhooks](https://api.slack.com/messaging/webhooks).
|
|
39
|
+
|
|
40
|
+
### Discord — Webhook URL
|
|
41
|
+
|
|
42
|
+
1. Open your Discord server.
|
|
43
|
+
2. Go to **Server settings** (gear by the server name) → **Integrations** → **Webhooks** (or right‑click the channel → **Edit channel** → **Integrations** → **Webhooks**).
|
|
44
|
+
3. Click **New Webhook** (or **Create Webhook**).
|
|
45
|
+
4. Name it (e.g. “Agent notifications”), choose the **channel**, then **Copy Webhook URL**.
|
|
46
|
+
|
|
47
|
+
The URL looks like: `https://discord.com/api/webhooks/1234567890123456789/AbCdEfGhIjKlMnOpQrStUvWxYz...`
|
|
48
|
+
|
|
49
|
+
That’s your **WEBHOOK_URL** for Discord.
|
|
50
|
+
|
|
51
|
+
Docs: [Discord Webhooks](https://discord.com/developers/docs/resources/webhook).
|
|
52
|
+
|
|
53
|
+
## Send a message (script)
|
|
54
|
+
|
|
55
|
+
Use the **exec** tool with **skill_id: "webhook"** and a **command** that runs the script. The script receives the JSON body as its first argument; `WEBHOOK_URL` comes from skill config (injected as env).
|
|
56
|
+
|
|
57
|
+
**Example (Slack-style):**
|
|
58
|
+
|
|
59
|
+
- `skill_id`: `webhook`
|
|
60
|
+
- `command`: `./scripts/post.sh '{"text":"Hello from the agent"}'`
|
|
61
|
+
|
|
62
|
+
**Example (Discord-style):**
|
|
63
|
+
|
|
64
|
+
- `skill_id`: `webhook`
|
|
65
|
+
- `command`: `./scripts/post.sh '{"content":"Reminder: standup in 5 min"}'`
|
|
66
|
+
|
|
67
|
+
Slack incoming webhooks expect:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{"text": "Your message here"}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Discord webhooks often expect:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{"content": "Your message here"}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Or with username:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{"content": "Reminder: standup in 5 min", "username": "Agent"}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Use the format required by your endpoint. The script sends the body as-is with `Content-Type: application/json`.
|
|
86
|
+
|
|
87
|
+
## Direct curl (if URL is not sensitive)
|
|
88
|
+
|
|
89
|
+
If the user provides the webhook URL in the message (e.g. "post this to https://hooks.slack.com/..."), you can use exec **without** skill_id and run curl directly:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
curl -sS -X POST -H "Content-Type: application/json" -d '{"text":"Message"}' "https://user-provided-url"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Prefer the script + skill config when the URL should stay private.
|
|
96
|
+
|
|
97
|
+
## Tips
|
|
98
|
+
|
|
99
|
+
- Escape the JSON properly in the shell: use single quotes around the JSON and escape any single quotes inside (e.g. `'"'"'` to embed a quote).
|
|
100
|
+
- For Slack, keep the payload to `{"text": "..."}` for simple messages.
|
|
101
|
+
- For "notify me when X" or "remind me", send a short, clear message to the webhook; the agent does not schedule future notifications (that would require a scheduler or external service).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"properties": {
|
|
4
|
+
"WEBHOOK_URL": {
|
|
5
|
+
"type": "string",
|
|
6
|
+
"title": "Webhook URL",
|
|
7
|
+
"description": "Full URL to POST to (e.g. Slack incoming webhook, Discord webhook, or any HTTP endpoint). Set in skill config so the agent can send notifications without seeing the URL."
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"required": ["WEBHOOK_URL"]
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# POST a JSON body to WEBHOOK_URL (from skill config, injected as env by exec).
|
|
3
|
+
# Usage: post.sh '<json>'
|
|
4
|
+
# Example: post.sh '{"text":"Hello from the agent"}'
|
|
5
|
+
if [ -z "$WEBHOOK_URL" ]; then
|
|
6
|
+
echo "WEBHOOK_URL is not set. Configure it in the webhook skill settings." >&2
|
|
7
|
+
exit 1
|
|
8
|
+
fi
|
|
9
|
+
if [ -z "$1" ]; then
|
|
10
|
+
echo "Usage: post.sh '<json body>'" >&2
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
curl -sS -X POST -H "Content-Type: application/json" -d "$1" "$WEBHOOK_URL"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: youtube
|
|
3
|
+
description: Upload videos (or Shorts) to YouTube using a local OAuth setup. Uses a script in the skill directory; run it via the exec tool. Use when the user wants to upload a video, publish to YouTube, or upload Shorts.
|
|
4
|
+
credentials:
|
|
5
|
+
- YOUTUBE_CLIENT_SECRET_JSON
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# YouTube upload
|
|
10
|
+
|
|
11
|
+
Upload videos to **YouTube** using a **script** in this skill. Credentials are stored in the skill directory (`client_secret.json` + `token.json`). Use the **exec** tool to run the script; no static upload code in the loader.
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
1. **One-time setup**: Enable YouTube Data API v3, create an OAuth Desktop client, add the client JSON via Skills → Configure (or place `client_secret.json` in the skill). Install Node dependencies with **npm install** (see below). Run the upload script once to complete browser OAuth; `token.json` is saved for reuse.
|
|
16
|
+
2. **Upload**: Use **exec** with `skill_id: "youtube"` and a `command` that runs **node scripts/youtube_upload.js** (recommended) or the Python script, with `--file`, `--title`, and optional `--description`, `--tags`, `--privacy`.
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
Follow these steps so the skill can upload to your YouTube account. The **Skills → YouTube → Configure** dialog (Setup button) shows this section and a field for the client JSON.
|
|
21
|
+
|
|
22
|
+
1. **Google Cloud project**
|
|
23
|
+
Go to [Google Cloud Console](https://console.cloud.google.com/). Create or select a project.
|
|
24
|
+
|
|
25
|
+
2. **Enable YouTube Data API v3**
|
|
26
|
+
APIs & Services → Library → search “YouTube Data API v3” → Enable.
|
|
27
|
+
|
|
28
|
+
3. **Create OAuth 2.0 credentials**
|
|
29
|
+
APIs & Services → Credentials → Create Credentials → **OAuth client ID**.
|
|
30
|
+
- If asked, configure the OAuth consent screen (e.g. External, add your email).
|
|
31
|
+
- Application type: **Web application** (so you can set a redirect URI).
|
|
32
|
+
- Name: e.g. “YouTube upload”.
|
|
33
|
+
- Under **Authorized redirect URIs**, click Add URI and add exactly:
|
|
34
|
+
**`http://localhost:8090/`**
|
|
35
|
+
(If this is missing, you will get “Error 400: redirect_uri_mismatch” when signing in.)
|
|
36
|
+
- Create. Copy or download the client JSON.
|
|
37
|
+
|
|
38
|
+
4. **Add the client JSON to this skill**
|
|
39
|
+
Paste the **full JSON** (the whole file content) into the **OAuth client JSON** field below (Skills → YouTube → Configure), then Save.
|
|
40
|
+
Alternatively, save the file as `scripts/client_secret.json` in the YouTube skill directory.
|
|
41
|
+
|
|
42
|
+
5. **Install Node dependencies**
|
|
43
|
+
From the skill directory run: `npm install`. The agent can do this via exec with `skill_id: "youtube"`, `command: "npm install"`.
|
|
44
|
+
|
|
45
|
+
6. **First sign-in**
|
|
46
|
+
Run the upload script once (e.g. with a test video). A browser opens; sign in with your Google account and allow access. After that, `token.json` is saved and future uploads do not require signing in again.
|
|
47
|
+
|
|
48
|
+
Details: [references/youtube-upload.md](references/youtube-upload.md).
|
|
49
|
+
|
|
50
|
+
## Install dependencies (agent must do this when needed)
|
|
51
|
+
|
|
52
|
+
**Preferred (Node):** If the Node script fails with "Missing npm dependencies", or before first upload, install with **exec** and `skill_id: "youtube"`, `command: "npm install"`. Then retry. No system Python or pip needed.
|
|
53
|
+
|
|
54
|
+
**Optional (Python):** If using the Python script and it reports missing Google API packages, use a virtual environment; the agent cannot install into system Python on externally-managed systems (e.g. macOS).
|
|
55
|
+
|
|
56
|
+
## Upload video (exec tool)
|
|
57
|
+
|
|
58
|
+
Use **exec** with `skill_id: "youtube"` (command runs in the skill directory).
|
|
59
|
+
|
|
60
|
+
**Recommended — Node script (no Python/pip):**
|
|
61
|
+
`node scripts/youtube_upload.js --file <path> --title "<title>" [--description "<desc>"] [--tags "tag1,tag2"] [--privacy public|private|unlisted]`
|
|
62
|
+
|
|
63
|
+
Example:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"skill_id": "youtube",
|
|
68
|
+
"command": "node scripts/youtube_upload.js --file /path/to/video.mp4 --title \"My video\" --description \"Optional\" --tags \"shorts,demo\" --privacy public"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Optional — Python script** (requires venv + pip): `python3 scripts/youtube_upload.py --file ... --title ...`
|
|
73
|
+
|
|
74
|
+
- **--file**: Path to the video file (absolute or relative to skill dir). Must be reachable where the agent runs.
|
|
75
|
+
- **--title**: Required. **--description**, **--tags**, **--privacy** optional (default: `public`).
|
|
76
|
+
|
|
77
|
+
**Interpreting exec result:** After running the upload script, check **stdout**. If it contains a line like `Uploaded: https://youtube.com/watch?v=...`, the upload **succeeded**. Tell the user the video was uploaded and give them that URL. Do not say authorization is required when stdout shows a success URL. If exitCode is non-zero but stdout contains the upload URL, still report success (the script may have been interrupted after writing the URL). Only when stdout has no upload URL and stderr says "Open this URL in your browser" should you ask the user to authorize.
|
|
78
|
+
|
|
79
|
+
Full script options: [references/youtube-upload.md](references/youtube-upload.md).
|
|
80
|
+
|
|
81
|
+
## Skill layout
|
|
82
|
+
|
|
83
|
+
- **scripts/youtube_upload.js** — Node upload script (recommended): `--file`, `--title`, `--description`, `--tags`, `--privacy`. No Python/pip required.
|
|
84
|
+
- **scripts/youtube_upload.py** — Python upload script (optional; requires venv + pip install).
|
|
85
|
+
- **package.json** — Node dependencies (googleapis). Run `npm install` in the skill dir.
|
|
86
|
+
- **config.schema.json** — defines `YOUTUBE_CLIENT_SECRET_JSON` for the Skills config UI.
|
|
87
|
+
- **scripts/client_secret.json** — (optional) OAuth client JSON if not set via Skills → Configure.
|
|
88
|
+
- **scripts/token.json** — (created on first run) stored credentials; shared by both scripts.
|
|
89
|
+
- **requirements.txt** — pip dependencies for the Python script only.
|
|
90
|
+
- **references/youtube-upload.md** — setup, usage, and exec examples.
|
|
91
|
+
- **SKILL.md** — this file.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "object",
|
|
3
|
+
"properties": {
|
|
4
|
+
"YOUTUBE_CLIENT_SECRET_JSON": {
|
|
5
|
+
"type": "string",
|
|
6
|
+
"format": "password",
|
|
7
|
+
"title": "OAuth client JSON",
|
|
8
|
+
"description": "Paste the full content of client_secret.json from Google Cloud Console (Desktop OAuth 2.0 client, YouTube Data API v3 enabled). Alternatively place the file as scripts/client_secret.json in this skill directory."
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# YouTube upload script
|
|
2
|
+
|
|
3
|
+
Upload a video to YouTube using OAuth credentials. The script reads credentials from **Skills config** (env `YOUTUBE_CLIENT_SECRET_JSON`, set via Dashboard → Skills → YouTube → Configure) or from `scripts/client_secret.json`, and saves `token.json` after the first browser OAuth flow.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- **Google Cloud project** with YouTube Data API v3 enabled.
|
|
8
|
+
- **OAuth 2.0 Client** (Desktop app): add the JSON via **Skills → YouTube → Configure** (paste full content), or save as `data/skills/youtube/scripts/client_secret.json`.
|
|
9
|
+
- **Python 3** with pip packages from `data/skills/youtube/requirements.txt`:
|
|
10
|
+
```bash
|
|
11
|
+
pip install -r data/skills/youtube/requirements.txt
|
|
12
|
+
```
|
|
13
|
+
Or: `google-api-python-client`, `google-auth-oauthlib`, `google-auth-httplib2`.
|
|
14
|
+
|
|
15
|
+
## One-time setup
|
|
16
|
+
|
|
17
|
+
1. In [Google Cloud Console](https://console.cloud.google.com/): create or select a project → APIs & Services → Enable **YouTube Data API v3**.
|
|
18
|
+
2. Create **OAuth 2.0 Client ID** (Application type: Desktop app). Download the JSON.
|
|
19
|
+
3. Add the JSON via **Skills → YouTube → Configure** (paste the full content), or save as `client_secret.json` in the skill’s **scripts** directory.
|
|
20
|
+
4. Run the script once with any test args; a browser will open to sign in. After authorizing, `token.json` is created in the same directory. Future runs use this token (refreshed automatically).
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
From the skill directory (or use **exec** with `skill_id: "youtube"` so cwd is the skill dir):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
python3 scripts/youtube_upload.py \
|
|
28
|
+
--file /path/to/video.mp4 \
|
|
29
|
+
--title "My video title" \
|
|
30
|
+
--description "Optional description" \
|
|
31
|
+
--tags "shorts,demo" \
|
|
32
|
+
--privacy public
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Options
|
|
36
|
+
|
|
37
|
+
| Option | Required | Description |
|
|
38
|
+
|----------------|----------|--------------------------------------------------|
|
|
39
|
+
| `--file` | Yes | Path to video file (absolute or relative to script dir). |
|
|
40
|
+
| `--title` | Yes | Video title. |
|
|
41
|
+
| `--description`| No | Video description (default: empty). |
|
|
42
|
+
| `--tags` | No | Comma-separated tags (e.g. `shorts,demo`). |
|
|
43
|
+
| `--privacy` | No | `public`, `private`, or `unlisted` (default: public). |
|
|
44
|
+
|
|
45
|
+
## Agent usage (exec tool)
|
|
46
|
+
|
|
47
|
+
Use the **exec** tool with `skill_id: "youtube"` so the command runs in the YouTube skill directory:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"skill_id": "youtube",
|
|
52
|
+
"command": "python3 scripts/youtube_upload.py --file /path/to/video.mp4 --title \"My title\" --description \"Description\" --tags \"tag1,tag2\" --privacy public"
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Ensure the video file path is accessible from the environment where the agent runs (e.g. workspace path or absolute path). Quotes inside the command string must be escaped as required by the shell.
|
|
57
|
+
|
|
58
|
+
## Troubleshooting
|
|
59
|
+
|
|
60
|
+
| Problem | Cause | Action |
|
|
61
|
+
|----------------------------|---------------------------------|---------------------------------------------|
|
|
62
|
+
| client_secret.json not found / YOUTUBE_CLIENT_SECRET_JSON not set | Credentials missing | Add via Skills → YouTube → Configure or place `scripts/client_secret.json`. |
|
|
63
|
+
| Token expired / invalid | First run or token revoked | Delete `token.json` and run again to re-auth. |
|
|
64
|
+
| File not found | Wrong path for `--file` | Use absolute path or path relative to script dir. |
|
|
65
|
+
| Quota exceeded | YouTube API quota | Check quota in Cloud Console; wait or request increase. |
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Upload a video to YouTube. Uses OAuth credentials from YOUTUBE_CLIENT_SECRET_JSON
|
|
4
|
+
* (Skills config) or scripts/client_secret.json. Saves token.json in scripts/ for reuse.
|
|
5
|
+
*
|
|
6
|
+
* Usage: node scripts/youtube_upload.js --file VIDEO.mp4 --title "Title" [--description "..." ] [--tags "a,b"] [--privacy public|private|unlisted]
|
|
7
|
+
*
|
|
8
|
+
* Dependencies: npm install (in the skill directory). Agent can run: exec with skill_id "youtube", command "npm install".
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require("fs");
|
|
12
|
+
const path = require("path");
|
|
13
|
+
const http = require("http");
|
|
14
|
+
|
|
15
|
+
const SCRIPT_DIR = path.resolve(path.dirname(__filename));
|
|
16
|
+
const SKILL_DIR = path.resolve(SCRIPT_DIR, "..");
|
|
17
|
+
const TOKEN_PATH = path.join(SCRIPT_DIR, "token.json");
|
|
18
|
+
const SCOPES = ["https://www.googleapis.com/auth/youtube.upload"];
|
|
19
|
+
const REDIRECT_URI = "http://localhost:8090/";
|
|
20
|
+
|
|
21
|
+
function parseArgs() {
|
|
22
|
+
const args = process.argv.slice(2);
|
|
23
|
+
const out = { file: null, title: null, description: "", tags: "", privacy: "public" };
|
|
24
|
+
for (let i = 0; i < args.length; i++) {
|
|
25
|
+
if (args[i] === "--file" && args[i + 1]) out.file = args[++i];
|
|
26
|
+
else if (args[i] === "--title" && args[i + 1]) out.title = args[++i];
|
|
27
|
+
else if (args[i] === "--description" && args[i + 1]) out.description = args[++i];
|
|
28
|
+
else if (args[i] === "--tags" && args[i + 1]) out.tags = args[++i];
|
|
29
|
+
else if (args[i] === "--privacy" && args[i + 1]) out.privacy = args[++i];
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getClientSecret() {
|
|
35
|
+
const fromEnv = process.env.YOUTUBE_CLIENT_SECRET_JSON;
|
|
36
|
+
if (fromEnv && fromEnv.trim()) {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(fromEnv.trim());
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.error("ERROR: YOUTUBE_CLIENT_SECRET_JSON is not valid JSON.", e.message);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const p = path.join(SCRIPT_DIR, "client_secret.json");
|
|
45
|
+
if (!fs.existsSync(p)) {
|
|
46
|
+
console.error("ERROR: client_secret.json not found and YOUTUBE_CLIENT_SECRET_JSON not set.");
|
|
47
|
+
console.error("Add via Skills → YouTube → Configure or place scripts/client_secret.json.");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getOAuth2Client(credentials) {
|
|
54
|
+
const { google } = require("googleapis");
|
|
55
|
+
const client = credentials.installed || credentials.web;
|
|
56
|
+
if (!client) {
|
|
57
|
+
console.error("ERROR: client_secret JSON must have 'installed' or 'web' with client_id and client_secret.");
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
return new google.auth.OAuth2(client.client_id, client.client_secret, REDIRECT_URI);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function loadSavedToken() {
|
|
64
|
+
if (!fs.existsSync(TOKEN_PATH)) return null;
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(fs.readFileSync(TOKEN_PATH, "utf8"));
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function saveToken(tokens) {
|
|
73
|
+
fs.writeFileSync(TOKEN_PATH, JSON.stringify(tokens, null, 2), "utf8");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function authorize(oauth2Client) {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const authUrl = oauth2Client.generateAuthUrl({
|
|
79
|
+
access_type: "offline",
|
|
80
|
+
scope: SCOPES,
|
|
81
|
+
prompt: "consent",
|
|
82
|
+
});
|
|
83
|
+
const server = http
|
|
84
|
+
.createServer(async (req, res) => {
|
|
85
|
+
const url = new URL(req.url || "", `http://localhost`);
|
|
86
|
+
const code = url.searchParams.get("code");
|
|
87
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
88
|
+
if (code) {
|
|
89
|
+
res.end("Authorized. You can close this tab.");
|
|
90
|
+
server.close();
|
|
91
|
+
try {
|
|
92
|
+
const { tokens } = await oauth2Client.getToken(code);
|
|
93
|
+
oauth2Client.setCredentials(tokens);
|
|
94
|
+
saveToken(tokens);
|
|
95
|
+
resolve(oauth2Client);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
reject(e);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
res.end("Missing code. Try again.");
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
.listen(8090, "localhost", () => {
|
|
104
|
+
console.error("Open this URL in your browser to authorize:");
|
|
105
|
+
console.error(authUrl);
|
|
106
|
+
const op = require("child_process").spawn(
|
|
107
|
+
process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open",
|
|
108
|
+
[authUrl],
|
|
109
|
+
{ stdio: "ignore" }
|
|
110
|
+
);
|
|
111
|
+
op.on("error", () => {});
|
|
112
|
+
});
|
|
113
|
+
server.on("error", reject);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function getAuthenticatedClient() {
|
|
118
|
+
let credentials;
|
|
119
|
+
try {
|
|
120
|
+
credentials = getClientSecret();
|
|
121
|
+
} catch (e) {
|
|
122
|
+
console.error("ERROR:", e.message);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
const oauth2Client = getOAuth2Client(credentials);
|
|
126
|
+
const token = loadSavedToken();
|
|
127
|
+
if (token) {
|
|
128
|
+
oauth2Client.setCredentials(token);
|
|
129
|
+
oauth2Client.on("tokens", (tokens) => {
|
|
130
|
+
const creds = oauth2Client.credentials;
|
|
131
|
+
if (tokens.refresh_token) creds.refresh_token = tokens.refresh_token;
|
|
132
|
+
saveToken(creds);
|
|
133
|
+
});
|
|
134
|
+
return oauth2Client;
|
|
135
|
+
}
|
|
136
|
+
return authorize(oauth2Client);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function uploadVideo(oauth2Client, filePath, title, description, tagsList, privacy) {
|
|
140
|
+
const { google } = require("googleapis");
|
|
141
|
+
const youtube = google.youtube({ version: "v3", auth: oauth2Client });
|
|
142
|
+
const body = {
|
|
143
|
+
snippet: {
|
|
144
|
+
title,
|
|
145
|
+
description: description || "",
|
|
146
|
+
tags: tagsList,
|
|
147
|
+
categoryId: "22",
|
|
148
|
+
},
|
|
149
|
+
status: { privacyStatus: privacy },
|
|
150
|
+
};
|
|
151
|
+
const media = {
|
|
152
|
+
body: fs.createReadStream(filePath),
|
|
153
|
+
};
|
|
154
|
+
const res = await youtube.videos.insert({
|
|
155
|
+
part: "snippet,status",
|
|
156
|
+
requestBody: body,
|
|
157
|
+
media,
|
|
158
|
+
});
|
|
159
|
+
const videoId = res.data.id;
|
|
160
|
+
console.log("Uploaded: https://youtube.com/watch?v=" + videoId);
|
|
161
|
+
return videoId;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function main() {
|
|
165
|
+
const args = parseArgs();
|
|
166
|
+
if (!args.file || !args.title) {
|
|
167
|
+
console.error("Usage: node youtube_upload.js --file <path> --title \"Title\" [--description \"...\"] [--tags \"a,b\"] [--privacy public|private|unlisted]");
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
if (!["public", "private", "unlisted"].includes(args.privacy)) {
|
|
171
|
+
console.error("ERROR: --privacy must be public, private, or unlisted");
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
let filePath = path.resolve(args.file);
|
|
175
|
+
if (!path.isAbsolute(args.file)) filePath = path.resolve(SCRIPT_DIR, args.file);
|
|
176
|
+
if (!fs.existsSync(filePath)) {
|
|
177
|
+
console.error("ERROR: File not found:", filePath);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
const tagsList = args.tags ? args.tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const auth = await getAuthenticatedClient();
|
|
184
|
+
await uploadVideo(auth, filePath, args.title, args.description, tagsList, args.privacy);
|
|
185
|
+
const creds = auth.credentials;
|
|
186
|
+
if (creds && (creds.access_token || creds.refresh_token)) saveToken(creds);
|
|
187
|
+
process.exit(0);
|
|
188
|
+
} catch (e) {
|
|
189
|
+
if (e.code === "MODULE_NOT_FOUND" && (e.message || "").includes("googleapis")) {
|
|
190
|
+
console.error("ERROR: Missing npm dependencies. Install with:");
|
|
191
|
+
console.error(" npm install");
|
|
192
|
+
console.error("Run that from the skill directory (exec with skill_id 'youtube', command 'npm install'), then retry.");
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
console.error("ERROR:", e.message || e);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
main();
|