@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.
Files changed (57) hide show
  1. package/README.md +1 -0
  2. package/data/agents/briefing_agent.json +12 -0
  3. package/data/agents/dev_agent.json +10 -0
  4. package/data/agents/manager_agent.json +11 -0
  5. package/data/agents/media_agent.json +10 -0
  6. package/data/agents/personal_agent.json +11 -0
  7. package/data/agents/research_agent.json +10 -0
  8. package/data/agents/social_media_agent.json +10 -0
  9. package/data/agents/writer_agent.json +10 -0
  10. package/data/skills/bluesky/SKILL.md +63 -0
  11. package/data/skills/bluesky/config.schema.json +17 -0
  12. package/data/skills/bluesky/scripts/post.sh +48 -0
  13. package/data/skills/bluesky/scripts/timeline.sh +37 -0
  14. package/data/skills/date/SKILL.md +53 -0
  15. package/data/skills/fetch/SKILL.md +42 -0
  16. package/data/skills/file-search/SKILL.md +122 -0
  17. package/data/skills/file-stats/SKILL.md +68 -0
  18. package/data/skills/git/SKILL.md +60 -0
  19. package/data/skills/gmail/SKILL.md +55 -0
  20. package/data/skills/gmail/references/send-email.md +54 -0
  21. package/data/skills/gmail/scripts/send_email.py +94 -0
  22. package/data/skills/hash/SKILL.md +56 -0
  23. package/data/skills/jq/SKILL.md +66 -0
  24. package/data/skills/markdown-to-html/SKILL.md +47 -0
  25. package/data/skills/memory/SKILL.md +64 -0
  26. package/data/skills/qr-code/SKILL.md +65 -0
  27. package/data/skills/rss/SKILL.md +40 -0
  28. package/data/skills/sulala-portal/SKILL.md +92 -0
  29. package/data/skills/translate/SKILL.md +52 -0
  30. package/data/skills/weather/SKILL.md +59 -0
  31. package/data/skills/web-search/SKILL.md +55 -0
  32. package/data/skills/web-search/config.schema.json +12 -0
  33. package/data/skills/web-search/scripts/search.sh +21 -0
  34. package/data/skills/webhook/SKILL.md +101 -0
  35. package/data/skills/webhook/config.schema.json +11 -0
  36. package/data/skills/webhook/scripts/post.sh +13 -0
  37. package/data/skills/youtube/SKILL.md +91 -0
  38. package/data/skills/youtube/config.schema.json +11 -0
  39. package/data/skills/youtube/package.json +8 -0
  40. package/data/skills/youtube/references/youtube-upload.md +65 -0
  41. package/data/skills/youtube/requirements.txt +3 -0
  42. package/data/skills/youtube/scripts/youtube_upload.js +200 -0
  43. package/data/skills/youtube/scripts/youtube_upload.py +125 -0
  44. package/data/templates/BOOT.md +11 -0
  45. package/data/templates/BOOTSTRAP.md +62 -0
  46. package/data/templates/HEARTBEAT.md +12 -0
  47. package/data/templates/IDENTITY.dev.md +62 -0
  48. package/data/templates/IDENTITY.md +60 -0
  49. package/data/templates/SYSTEM.dev.md +82 -0
  50. package/data/templates/SYSTEM.md +65 -0
  51. package/data/templates/TOOLS.dev.md +24 -0
  52. package/data/templates/TOOLS.md +47 -0
  53. package/data/templates/USER.dev.md +18 -0
  54. package/data/templates/USER.md +23 -0
  55. package/dist/cli.js +22 -13
  56. package/dist/index.js +14 -5
  57. 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&current_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,8 @@
1
+ {
2
+ "name": "youtube-skill",
3
+ "private": true,
4
+ "description": "YouTube upload script dependencies",
5
+ "dependencies": {
6
+ "googleapis": "^144.0.0"
7
+ }
8
+ }
@@ -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,3 @@
1
+ google-api-python-client
2
+ google-auth-oauthlib
3
+ google-auth-httplib2
@@ -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();