create-ironclaws 1.0.3 → 1.1.0

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.
@@ -1,174 +0,0 @@
1
- """
2
- Standalone YouTube tool for Argus agents.
3
- Search YouTube channel videos and fetch transcripts.
4
-
5
- Usage:
6
- # Search for videos in a channel
7
- python3 youtube_tool.py search --query "security overview" --channel UC_CHANNEL_ID
8
-
9
- # Get transcript for a specific video
10
- python3 youtube_tool.py transcript --video-id dQw4w9WgXcQ
11
-
12
- Environment variables (required):
13
- GOOGLE_ACCESS_TOKEN Google OAuth2 access token (with YouTube Data API scope)
14
-
15
- Optional (for transcript fallback if youtube-transcript-api needs no auth):
16
- None — transcript fetching uses youtube-transcript-api which is unauthenticated.
17
- """
18
-
19
- import argparse
20
- import os
21
- import sys
22
-
23
- try:
24
- import requests
25
- except ImportError:
26
- print("ERROR: 'requests' is not installed.", file=sys.stderr)
27
- sys.exit(1)
28
-
29
- YOUTUBE_API_BASE = "https://www.googleapis.com/youtube/v3"
30
-
31
-
32
- def get_access_token():
33
- """Get Google access token for YouTube Data API."""
34
- token = os.environ.get("GOOGLE_ACCESS_TOKEN", "").strip()
35
- if not token:
36
- print("ERROR: GOOGLE_ACCESS_TOKEN must be set.", file=sys.stderr)
37
- sys.exit(1)
38
- return token
39
-
40
-
41
- def cmd_search(args):
42
- token = get_access_token()
43
- query = args.query
44
- channel_id = args.channel
45
-
46
- params = {
47
- "part": "snippet",
48
- "q": query,
49
- "type": "video",
50
- "maxResults": 10,
51
- "order": "relevance",
52
- }
53
- if channel_id:
54
- params["channelId"] = channel_id
55
-
56
- resp = requests.get(
57
- f"{YOUTUBE_API_BASE}/search",
58
- headers={"Authorization": f"Bearer {token}"},
59
- params=params,
60
- timeout=30,
61
- )
62
- resp.raise_for_status()
63
- data = resp.json()
64
-
65
- items = data.get("items", [])
66
- if not items:
67
- print("No videos found matching your query.")
68
- return
69
-
70
- print(f"# Search Results ({len(items)} videos)\n")
71
- for item in items:
72
- video_id = item.get("id", {}).get("videoId", "")
73
- snippet = item.get("snippet", {})
74
- title = snippet.get("title", "Untitled")
75
- description = snippet.get("description", "")
76
- published = snippet.get("publishedAt", "")
77
- channel_title = snippet.get("channelTitle", "")
78
- video_url = f"https://www.youtube.com/watch?v={video_id}"
79
-
80
- print(f"## {title}")
81
- print(f"**Video ID:** {video_id}")
82
- print(f"**URL:** {video_url}")
83
- if channel_title:
84
- print(f"**Channel:** {channel_title}")
85
- if published:
86
- print(f"**Published:** {published}")
87
- if description:
88
- print(f"**Description:** {description}")
89
- print()
90
- print("---\n")
91
-
92
-
93
- def cmd_transcript(args):
94
- video_id = args.video_id
95
-
96
- # First, try to get video metadata via YouTube Data API (if token available)
97
- title = ""
98
- token = os.environ.get("GOOGLE_ACCESS_TOKEN", "").strip()
99
- if token:
100
- try:
101
- resp = requests.get(
102
- f"{YOUTUBE_API_BASE}/videos",
103
- headers={"Authorization": f"Bearer {token}"},
104
- params={"part": "snippet", "id": video_id},
105
- timeout=30,
106
- )
107
- resp.raise_for_status()
108
- items = resp.json().get("items", [])
109
- if items:
110
- title = items[0].get("snippet", {}).get("title", "")
111
- except Exception:
112
- pass # Non-fatal — we still want the transcript
113
-
114
- video_url = f"https://www.youtube.com/watch?v={video_id}"
115
-
116
- # Fetch transcript using youtube-transcript-api
117
- try:
118
- from youtube_transcript_api import YouTubeTranscriptApi
119
- except ImportError:
120
- print(
121
- "ERROR: 'youtube-transcript-api' is not installed. "
122
- "Install it with: pip install youtube-transcript-api",
123
- file=sys.stderr,
124
- )
125
- sys.exit(1)
126
-
127
- try:
128
- transcript_list = YouTubeTranscriptApi.get_transcript(video_id)
129
- except Exception as e:
130
- print(f"ERROR: Could not fetch transcript for video {video_id}: {e}", file=sys.stderr)
131
- sys.exit(1)
132
-
133
- # Format transcript as plain text
134
- transcript_text = "\n".join(entry.get("text", "") for entry in transcript_list)
135
-
136
- if title:
137
- print(f"# {title}")
138
- print(f"**Video URL:** {video_url}")
139
- print(f"**Video ID:** {video_id}")
140
- print(f"\n## Transcript\n")
141
- print(transcript_text)
142
-
143
-
144
- def parse_args():
145
- p = argparse.ArgumentParser(description="YouTube tool for Argus agents")
146
- sub = p.add_subparsers(dest="command")
147
-
148
- s = sub.add_parser("search", help="Search YouTube videos")
149
- s.add_argument("--query", required=True, help="Search query text")
150
- s.add_argument("--channel", default=None, help="YouTube channel ID to search within (optional)")
151
-
152
- t = sub.add_parser("transcript", help="Get transcript for a YouTube video")
153
- t.add_argument("--video-id", required=True, help="YouTube video ID")
154
-
155
- return p.parse_args()
156
-
157
-
158
- def main():
159
- args = parse_args()
160
- if not args.command:
161
- print("ERROR: specify a command (search or transcript). Use --help for usage.", file=sys.stderr)
162
- sys.exit(1)
163
- try:
164
- {"search": cmd_search, "transcript": cmd_transcript}[args.command](args)
165
- except requests.HTTPError as e:
166
- print(f"ERROR: YouTube API error: {e}\n{e.response.text}", file=sys.stderr)
167
- sys.exit(1)
168
- except Exception as e:
169
- print(f"ERROR: {e}", file=sys.stderr)
170
- sys.exit(1)
171
-
172
-
173
- if __name__ == "__main__":
174
- main()
@@ -1,121 +0,0 @@
1
- #!/usr/bin/env bash
2
- # register-expense-agent.sh
3
- #
4
- # One-command setup for the expense-policy-checker agent.
5
- # Run this ONCE after you have:
6
- # 1. Created the private Slack channel and added Linn + Elisabeth
7
- # 2. Added Expensify credentials to .env
8
- # 3. Ran sync-to-vm.sh to push the latest files
9
- #
10
- # Usage (on VM):
11
- # EXPENSE_CHANNEL_ID=C... bash scripts/register-expense-agent.sh
12
- #
13
- # Or set directly:
14
- # EXPENSE_CHANNEL_ID=C0987654321 bash scripts/register-expense-agent.sh
15
-
16
- set -euo pipefail
17
-
18
- CHANNEL_ID="${EXPENSE_CHANNEL_ID:?Set EXPENSE_CHANNEL_ID=C...}"
19
- ENV_FILE="${ENV_FILE:-$HOME/nanoclaw-docker/.env}"
20
- API="http://localhost:10254"
21
-
22
- echo "=== Registering expense-policy-checker agent ==="
23
- echo ""
24
-
25
- cd ~/nanoclaw-docker
26
-
27
- # 1. Register in NanoClaw database
28
- echo "Registering agent in NanoClaw..."
29
- npx tsx setup/index.ts register \
30
- --jid "slack:${CHANNEL_ID}" \
31
- --name "Expense Policy Checker" \
32
- --trigger "@Argus" \
33
- --folder "expense-policy-checker" \
34
- --channel slack \
35
- --no-trigger-required
36
- echo " ✓ Agent registered"
37
-
38
- # 2. Register Expensify secret in OneCLI
39
- # Note: Expensify uses credentials in JSON request bodies, not HTTP headers.
40
- # OneCLI cannot inject body params, so these are passed as env vars.
41
- # We still register a marker secret so the agent identity is tracked in OneCLI.
42
- echo ""
43
- echo "Registering OneCLI agent for expense-policy-checker..."
44
-
45
- RAND_TOKEN=$(openssl rand -hex 20)
46
- EXISTING=$(docker exec onecli-postgres-1 psql -U onecli -d onecli -t -c \
47
- "SELECT COUNT(*) FROM agents WHERE name='expense-policy-checker';" | tr -d ' \n')
48
-
49
- if [ "$EXISTING" = "0" ]; then
50
- docker exec onecli-postgres-1 psql -U onecli -d onecli -c \
51
- "INSERT INTO agents (id, name, access_token, identifier, project_id, created_at, updated_at)
52
- VALUES ('nanoclaw-expense', 'expense-policy-checker', 'aoc_${RAND_TOKEN}',
53
- 'expense-policy-checker', (SELECT id FROM projects LIMIT 1), NOW(), NOW())
54
- ON CONFLICT (id) DO NOTHING;" > /dev/null
55
- echo " ✓ OneCLI agent created"
56
- else
57
- echo " ✓ OneCLI agent already exists"
58
- fi
59
-
60
- # Link litellm (LLM gateway) to expense-policy-checker
61
- docker exec onecli-postgres-1 psql -U onecli -d onecli -c "
62
- INSERT INTO agent_secrets (agent_id, secret_id, created_at, updated_at)
63
- SELECT 'nanoclaw-expense', id, NOW(), NOW()
64
- FROM secrets WHERE name = 'litellm'
65
- ON CONFLICT DO NOTHING;
66
- " > /dev/null
67
- echo " ✓ LiteLLM secret linked"
68
-
69
- # 3. Update sender allowlist to allow Linn and Elisabeth
70
- echo ""
71
- echo "Updating sender allowlist..."
72
- ALLOWLIST_FILE="$HOME/.config/nanoclaw/sender-allowlist.json"
73
- if [ -f "$ALLOWLIST_FILE" ]; then
74
- # Add the channel with no sender restrictions (Linn + Elisabeth are the only members)
75
- CURRENT=$(cat "$ALLOWLIST_FILE")
76
- CHANNEL_ENTRY="\"slack:${CHANNEL_ID}\": {\"mode\": \"allow\", \"senders\": []}"
77
- if echo "$CURRENT" | grep -q "$CHANNEL_ID"; then
78
- echo " ✓ Channel already in allowlist"
79
- else
80
- echo " ✓ Add this to ~/.config/nanoclaw/sender-allowlist.json manually:"
81
- echo " \"slack:${CHANNEL_ID}\": {\"mode\": \"allow\", \"senders\": []}"
82
- fi
83
- else
84
- echo " ⚠ No sender-allowlist.json found at $ALLOWLIST_FILE — create it manually"
85
- fi
86
-
87
- # 4. Set up scheduled tasks
88
- echo ""
89
- echo "Creating scheduled tasks..."
90
-
91
- # Daily compliance check (9am Oslo time, Mon-Fri)
92
- npx tsx setup/index.ts schedule-task \
93
- --jid "slack:${CHANNEL_ID}" \
94
- --prompt "Run the daily expense compliance check. Fetch reports for the last ${CHECK_LOOKBACK_DAYS:-7} days, inspect receipts, check policy, notify submitters and approvers of any violations, and update the state file." \
95
- --cron "0 9 * * 1-5" \
96
- --name "Daily expense compliance check" 2>/dev/null && \
97
- echo " ✓ Daily check scheduled (9am Mon-Fri Oslo time)" || \
98
- echo " ⚠ Could not schedule daily task — set it up via Global Claw or manually"
99
-
100
- # Weekly report (Monday 8am)
101
- npx tsx setup/index.ts schedule-task \
102
- --jid "slack:${CHANNEL_ID}" \
103
- --prompt "Generate and post the weekly expense compliance report to the finance Slack channel." \
104
- --cron "0 8 * * 1" \
105
- --name "Weekly expense report" 2>/dev/null && \
106
- echo " ✓ Weekly report scheduled (8am Mondays)" || \
107
- echo " ⚠ Could not schedule weekly task — set it up via Global Claw or manually"
108
-
109
- echo ""
110
- echo "=== Done! ==="
111
- echo ""
112
- echo "Next steps:"
113
- echo " 1. Add Expensify credentials to .env on the VM:"
114
- echo " EXPENSIFY_PARTNER_USER_ID=..."
115
- echo " EXPENSIFY_PARTNER_USER_SECRET=..."
116
- echo " EXPENSIFY_POLICY_ID=..."
117
- echo " EXPENSE_CHANNEL_ID=${CHANNEL_ID}"
118
- echo ""
119
- echo " 2. Restart NanoClaw: (kill tsx and restart npm run dev)"
120
- echo ""
121
- echo " 3. Test by sending 'run expense check' in the channel"
@@ -1,142 +0,0 @@
1
- #!/usr/bin/env bash
2
- # setup-agents.sh — Register all NanoClaw agents from agents.yaml.
3
- #
4
- # Reads agents.yaml and registers every agent whose channel ID env var is set.
5
- # Skips agents with no channel ID — safe to re-run at any time (idempotent).
6
- #
7
- # Usage:
8
- # # Register all agents you have channel IDs for:
9
- # ALERT_CHANNEL_ID=C... DEPS_CHANNEL_ID=C... ./setup-agents.sh
10
- #
11
- # # Register just one new agent (others already registered, will be re-registered safely):
12
- # EXPENSE_CHANNEL_ID=C... ./setup-agents.sh
13
- #
14
- # To add a new agent: add an entry in agents.yaml, set its channel ID env var, run this.
15
- # No new scripts needed.
16
- #
17
- # Channel IDs: right-click channel in Slack → View channel details → scroll to bottom.
18
- # DM IDs: open DM in Slack, check URL (starts with D...).
19
-
20
- set -euo pipefail
21
-
22
- cd "$(dirname "$0")"
23
-
24
- if ! command -v python3 &>/dev/null; then
25
- echo "ERROR: python3 required to parse agents.yaml"
26
- exit 1
27
- fi
28
-
29
- # Parse agents.yaml with Python (no yq dependency needed)
30
- AGENTS=$(python3 - << 'PYEOF'
31
- import yaml, os, sys, json
32
-
33
- with open("agents.yaml") as f:
34
- config = yaml.safe_load(f)
35
-
36
- for agent in config.get("agents", []):
37
- channel_env = agent.get("channel_env", "")
38
- channel_id = os.environ.get(channel_env, "").strip()
39
-
40
- if not channel_id:
41
- continue # Skip agents with no channel ID set
42
-
43
- print(json.dumps({
44
- "folder": agent["folder"],
45
- "name": agent["name"],
46
- "trigger": agent.get("trigger", "@Argus"),
47
- "channel_id": channel_id,
48
- "requires_trigger": agent.get("requires_trigger", False),
49
- "is_main": agent.get("is_main", False),
50
- "onecli_id": agent.get("onecli_id", ""),
51
- "onecli_secrets": agent.get("onecli_secrets", []),
52
- }))
53
- PYEOF
54
- )
55
-
56
- if [ -z "$AGENTS" ]; then
57
- echo "No agents to register — set at least one channel ID env var."
58
- echo ""
59
- echo "Example:"
60
- echo " ALERT_CHANNEL_ID=C... ./setup-agents.sh"
61
- echo ""
62
- echo "Available agents (from agents.yaml):"
63
- python3 -c "
64
- import yaml
65
- with open('agents.yaml') as f:
66
- config = yaml.safe_load(f)
67
- for a in config.get('agents', []):
68
- print(f\" {a['folder']:30s} → {a['channel_env']}\")
69
- "
70
- exit 0
71
- fi
72
-
73
- REGISTERED=0
74
- SKIPPED=0
75
-
76
- while IFS= read -r agent_json; do
77
- FOLDER=$(echo "$agent_json" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['folder'])")
78
- NAME=$(echo "$agent_json" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['name'])")
79
- TRIGGER=$(echo "$agent_json" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['trigger'])")
80
- CHANNEL_ID=$(echo "$agent_json" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['channel_id'])")
81
- IS_MAIN=$(echo "$agent_json" | python3 -c "import json,sys; d=json.load(sys.stdin); print('true' if d['is_main'] else 'false')")
82
- ONECLI_ID=$(echo "$agent_json" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['onecli_id'])")
83
- ONECLI_SECRETS=$(echo "$agent_json" | python3 -c "import json,sys; d=json.load(sys.stdin); print(' '.join(d['onecli_secrets']))")
84
-
85
- echo "Registering ${NAME} (${FOLDER}) → slack:${CHANNEL_ID}..."
86
-
87
- # Build register command
88
- REGISTER_ARGS=(
89
- --jid "slack:${CHANNEL_ID}"
90
- --name "${NAME}"
91
- --trigger "${TRIGGER}"
92
- --folder "${FOLDER}"
93
- --channel slack
94
- --no-trigger-required
95
- )
96
- [ "$IS_MAIN" = "true" ] && REGISTER_ARGS+=(--is-main)
97
-
98
- npx tsx setup/index.ts --step register -- "${REGISTER_ARGS[@]}"
99
-
100
- # Register in OneCLI if docker is available (VM setup)
101
- if command -v docker &>/dev/null && docker ps &>/dev/null 2>&1; then
102
- RAND_TOKEN=$(openssl rand -hex 20)
103
- EXISTING=$(docker exec onecli-postgres-1 psql -U onecli -d onecli -t -c \
104
- "SELECT COUNT(*) FROM agents WHERE name='${FOLDER}';" 2>/dev/null | tr -d ' \n' || echo "0")
105
-
106
- if [ "$EXISTING" = "0" ]; then
107
- AGENT_ID="${ONECLI_ID:-nanoclaw-${FOLDER}}"
108
- # Try new schema (account_id) first, fall back to old schema (project_id)
109
- docker exec onecli-postgres-1 psql -U onecli -d onecli -c \
110
- "INSERT INTO agents (id, name, access_token, identifier, secret_mode, account_id, created_at, updated_at)
111
- SELECT '${AGENT_ID}', '${FOLDER}', 'aoc_${RAND_TOKEN}', '${FOLDER}', 'selective', id, NOW(), NOW()
112
- FROM accounts LIMIT 1
113
- ON CONFLICT (id) DO NOTHING;" > /dev/null 2>&1 || \
114
- docker exec onecli-postgres-1 psql -U onecli -d onecli -c \
115
- "INSERT INTO agents (id, name, access_token, identifier, secret_mode, project_id, created_at, updated_at)
116
- VALUES ('${AGENT_ID}', '${FOLDER}', 'aoc_${RAND_TOKEN}', '${FOLDER}', 'selective',
117
- (SELECT id FROM projects LIMIT 1), NOW(), NOW())
118
- ON CONFLICT (id) DO NOTHING;" > /dev/null 2>&1 || true
119
- fi
120
-
121
- # Link OneCLI secrets
122
- for secret in $ONECLI_SECRETS; do
123
- docker exec onecli-postgres-1 psql -U onecli -d onecli -c "
124
- INSERT INTO agent_secrets (agent_id, secret_id, created_at, updated_at)
125
- SELECT a.id, s.id, NOW(), NOW()
126
- FROM agents a, secrets s
127
- WHERE a.name = '${FOLDER}' AND s.name = '${secret}'
128
- ON CONFLICT DO NOTHING;" > /dev/null 2>&1 || true
129
- done
130
- fi
131
-
132
- REGISTERED=$((REGISTERED + 1))
133
-
134
- done <<< "$AGENTS"
135
-
136
- echo ""
137
- echo "Done. Registered ${REGISTERED} agent(s)."
138
- if [ $SKIPPED -gt 0 ]; then
139
- echo "Skipped ${SKIPPED} agent(s) (no channel ID set)."
140
- fi
141
- echo ""
142
- echo "Next: restart NanoClaw to pick up the new registrations."