create-ironclaws 1.0.4 → 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.
- package/package.json +1 -1
- package/template/CLAUDE.md +105 -51
- package/template/container/Dockerfile.argus +14 -12
- package/template/container/skills/slack-formatting/SKILL.md +46 -47
- package/template/container/tools/README.md +33 -0
- package/template/docs/how-it-works.md +19 -19
- package/template/groups/forge/CLAUDE.md +13 -9
- package/template/scripts/setup-onecli-secrets.sh +25 -87
- package/template/src/container-runner.ts +4 -4
- package/template/src/index.ts +3 -3
- package/template/container/tools/elastic_query.py +0 -161
- package/template/container/tools/gdrive_tool.py +0 -185
- package/template/container/tools/jira_tool.py +0 -433
- package/template/container/tools/slack_history_tool.py +0 -144
- package/template/container/tools/youtube_tool.py +0 -174
- package/template/scripts/register-expense-agent.sh +0 -121
- package/template/setup-agents.sh +0 -142
|
@@ -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"
|
package/template/setup-agents.sh
DELETED
|
@@ -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."
|