create-ironclaws 1.0.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/README.md +101 -0
- package/bin/create.js +394 -0
- package/package.json +33 -0
- package/template/.env.example +38 -0
- package/template/CLAUDE.md +104 -0
- package/template/agent-credentials.yaml +33 -0
- package/template/agents.yaml +22 -0
- package/template/container/Dockerfile +70 -0
- package/template/container/Dockerfile.argus +34 -0
- package/template/container/agent-runner/package-lock.json +1524 -0
- package/template/container/agent-runner/package.json +23 -0
- package/template/container/agent-runner/src/index.ts +630 -0
- package/template/container/agent-runner/src/ipc-mcp-stdio.ts +339 -0
- package/template/container/agent-runner/tsconfig.json +15 -0
- package/template/container/build-argus.sh +25 -0
- package/template/container/build.sh +23 -0
- package/template/container/skills/agent-browser/SKILL.md +159 -0
- package/template/container/skills/agent-status/SKILL.md +69 -0
- package/template/container/skills/capabilities/SKILL.md +100 -0
- package/template/container/skills/edit-agent/SKILL.md +93 -0
- package/template/container/skills/slack-formatting/SKILL.md +92 -0
- package/template/container/skills/status/SKILL.md +104 -0
- package/template/container/tools/elastic_query.py +161 -0
- package/template/container/tools/gdrive_tool.py +185 -0
- package/template/container/tools/jira_tool.py +433 -0
- package/template/container/tools/slack_history_tool.py +144 -0
- package/template/container/tools/youtube_tool.py +174 -0
- package/template/docker-compose.yml +54 -0
- package/template/docs/how-it-works.md +496 -0
- package/template/eslint.config.js +32 -0
- package/template/groups/forge/CLAUDE.md +107 -0
- package/template/package-lock.json +5278 -0
- package/template/package.json +52 -0
- package/template/scripts/github-app-token.py +58 -0
- package/template/scripts/register-expense-agent.sh +121 -0
- package/template/scripts/run-migrations.ts +105 -0
- package/template/scripts/setup-onecli-secrets.sh +252 -0
- package/template/setup-agents.sh +142 -0
- package/template/src/channels/index.ts +13 -0
- package/template/src/channels/registry.test.ts +42 -0
- package/template/src/channels/registry.ts +28 -0
- package/template/src/channels/slack.test.ts +859 -0
- package/template/src/channels/slack.ts +373 -0
- package/template/src/claw-skill.test.ts +45 -0
- package/template/src/config.ts +94 -0
- package/template/src/container-runner.test.ts +221 -0
- package/template/src/container-runner.ts +1029 -0
- package/template/src/container-runtime.test.ts +149 -0
- package/template/src/container-runtime.ts +124 -0
- package/template/src/db-migration.test.ts +67 -0
- package/template/src/db.test.ts +484 -0
- package/template/src/db.ts +837 -0
- package/template/src/env.ts +42 -0
- package/template/src/formatting.test.ts +294 -0
- package/template/src/github-token.ts +48 -0
- package/template/src/google-token.ts +75 -0
- package/template/src/group-folder.test.ts +43 -0
- package/template/src/group-folder.ts +44 -0
- package/template/src/group-queue.test.ts +484 -0
- package/template/src/group-queue.ts +363 -0
- package/template/src/http-server.ts +343 -0
- package/template/src/index.ts +960 -0
- package/template/src/ipc-auth.test.ts +679 -0
- package/template/src/ipc.ts +548 -0
- package/template/src/logger.ts +16 -0
- package/template/src/mount-security.ts +421 -0
- package/template/src/network-policy.ts +119 -0
- package/template/src/remote-control.test.ts +397 -0
- package/template/src/remote-control.ts +224 -0
- package/template/src/router.ts +52 -0
- package/template/src/routing.test.ts +170 -0
- package/template/src/sender-allowlist.test.ts +216 -0
- package/template/src/sender-allowlist.ts +128 -0
- package/template/src/task-scheduler.test.ts +129 -0
- package/template/src/task-scheduler.ts +290 -0
- package/template/src/timezone.test.ts +73 -0
- package/template/src/timezone.ts +37 -0
- package/template/src/types.ts +114 -0
- package/template/src/worktree.ts +206 -0
- package/template/tsconfig.json +20 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Slack channel history tool for Byte agent.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
# Fetch last 180 days of messages
|
|
7
|
+
python3 slack_history_tool.py history --channel C05DV1DU58R --days 180
|
|
8
|
+
|
|
9
|
+
# Fetch messages since a specific date
|
|
10
|
+
python3 slack_history_tool.py history --channel C05DV1DU58R --since 2026-01-01
|
|
11
|
+
|
|
12
|
+
# Fetch messages since a Unix timestamp (for incremental reads)
|
|
13
|
+
python3 slack_history_tool.py history --channel C05DV1DU58R --oldest 1700000000
|
|
14
|
+
|
|
15
|
+
Environment variables (required):
|
|
16
|
+
SLACK_BOT_TOKEN Bot token with channels:history or groups:history scope
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
from datetime import datetime, timedelta
|
|
24
|
+
import urllib.request
|
|
25
|
+
import urllib.parse
|
|
26
|
+
|
|
27
|
+
SLACK_BOT_TOKEN = os.environ.get('SLACK_BOT_TOKEN', '')
|
|
28
|
+
|
|
29
|
+
_user_cache: dict = {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def slack_api(method: str, params: dict) -> dict:
|
|
33
|
+
url = f'https://slack.com/api/{method}'
|
|
34
|
+
data = urllib.parse.urlencode(params).encode()
|
|
35
|
+
req = urllib.request.Request(url, data=data, method='POST')
|
|
36
|
+
# Authorization is optional — if token absent, OneCLI HTTPS proxy injects it.
|
|
37
|
+
if SLACK_BOT_TOKEN:
|
|
38
|
+
req.add_header('Authorization', f'Bearer {SLACK_BOT_TOKEN}')
|
|
39
|
+
req.add_header('Content-Type', 'application/x-www-form-urlencoded')
|
|
40
|
+
with urllib.request.urlopen(req, timeout=30) as response:
|
|
41
|
+
return json.loads(response.read())
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def resolve_user(user_id: str) -> str:
|
|
45
|
+
if not user_id:
|
|
46
|
+
return 'Unknown'
|
|
47
|
+
if user_id in _user_cache:
|
|
48
|
+
return _user_cache[user_id]
|
|
49
|
+
try:
|
|
50
|
+
result = slack_api('users.info', {'user': user_id})
|
|
51
|
+
name = (
|
|
52
|
+
result.get('user', {}).get('real_name')
|
|
53
|
+
or result.get('user', {}).get('name')
|
|
54
|
+
or user_id
|
|
55
|
+
)
|
|
56
|
+
_user_cache[user_id] = name
|
|
57
|
+
return name
|
|
58
|
+
except Exception:
|
|
59
|
+
_user_cache[user_id] = user_id
|
|
60
|
+
return user_id
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def fetch_history(channel: str, oldest: str) -> list:
|
|
64
|
+
messages = []
|
|
65
|
+
cursor = None
|
|
66
|
+
|
|
67
|
+
while True:
|
|
68
|
+
params: dict = {
|
|
69
|
+
'channel': channel,
|
|
70
|
+
'limit': 200,
|
|
71
|
+
'oldest': oldest,
|
|
72
|
+
}
|
|
73
|
+
if cursor:
|
|
74
|
+
params['cursor'] = cursor
|
|
75
|
+
|
|
76
|
+
result = slack_api('conversations.history', params)
|
|
77
|
+
|
|
78
|
+
if not result.get('ok'):
|
|
79
|
+
print(f"Error: {result.get('error', 'unknown')}", file=sys.stderr)
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
messages.extend(result.get('messages', []))
|
|
83
|
+
|
|
84
|
+
if not result.get('has_more'):
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
cursor = result.get('response_metadata', {}).get('next_cursor')
|
|
88
|
+
if not cursor:
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
return messages
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def format_messages(messages: list) -> str:
|
|
95
|
+
lines = []
|
|
96
|
+
# API returns newest first — reverse for chronological order
|
|
97
|
+
for msg in reversed(messages):
|
|
98
|
+
# Skip system messages (joins, topic changes, etc.)
|
|
99
|
+
if msg.get('subtype'):
|
|
100
|
+
continue
|
|
101
|
+
text = msg.get('text', '').strip()
|
|
102
|
+
if not text:
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
ts = float(msg.get('ts', 0))
|
|
106
|
+
dt = datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M')
|
|
107
|
+
user_name = resolve_user(msg.get('user', ''))
|
|
108
|
+
|
|
109
|
+
lines.append(f'[{dt}] {user_name}: {text}')
|
|
110
|
+
|
|
111
|
+
return '\n'.join(lines)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def main():
|
|
115
|
+
# SLACK_BOT_TOKEN is optional — if absent, OneCLI HTTPS proxy injects Authorization.
|
|
116
|
+
parser = argparse.ArgumentParser(description='Fetch Slack channel history')
|
|
117
|
+
subparsers = parser.add_subparsers(dest='command')
|
|
118
|
+
|
|
119
|
+
history_parser = subparsers.add_parser('history', help='Fetch channel message history')
|
|
120
|
+
history_parser.add_argument('--channel', required=True, help='Slack channel ID')
|
|
121
|
+
history_parser.add_argument('--days', type=int, default=180, help='Number of days back (default: 180)')
|
|
122
|
+
history_parser.add_argument('--since', help='Fetch since this date (ISO format: 2026-01-01)')
|
|
123
|
+
history_parser.add_argument('--oldest', help='Fetch since this Unix timestamp')
|
|
124
|
+
|
|
125
|
+
args = parser.parse_args()
|
|
126
|
+
|
|
127
|
+
if args.command == 'history':
|
|
128
|
+
if args.oldest:
|
|
129
|
+
oldest = args.oldest
|
|
130
|
+
elif args.since:
|
|
131
|
+
dt = datetime.fromisoformat(args.since)
|
|
132
|
+
oldest = str(dt.timestamp())
|
|
133
|
+
else:
|
|
134
|
+
dt = datetime.now() - timedelta(days=args.days)
|
|
135
|
+
oldest = str(dt.timestamp())
|
|
136
|
+
|
|
137
|
+
messages = fetch_history(args.channel, oldest=oldest)
|
|
138
|
+
print(format_messages(messages))
|
|
139
|
+
else:
|
|
140
|
+
parser.print_help()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == '__main__':
|
|
144
|
+
main()
|
|
@@ -0,0 +1,174 @@
|
|
|
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()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# IronClaws infrastructure
|
|
2
|
+
# Starts OneCLI (credential proxy) and its PostgreSQL database.
|
|
3
|
+
# NanoClaw itself runs on the host: npm run dev
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# docker compose up -d # start
|
|
7
|
+
# docker compose down # stop
|
|
8
|
+
# docker compose logs onecli # check logs
|
|
9
|
+
|
|
10
|
+
services:
|
|
11
|
+
postgres:
|
|
12
|
+
image: postgres:18-alpine
|
|
13
|
+
environment:
|
|
14
|
+
POSTGRES_USER: onecli
|
|
15
|
+
POSTGRES_PASSWORD: onecli
|
|
16
|
+
POSTGRES_DB: onecli
|
|
17
|
+
volumes:
|
|
18
|
+
- onecli_postgres:/var/lib/postgresql/data
|
|
19
|
+
networks:
|
|
20
|
+
- onecli_net
|
|
21
|
+
restart: unless-stopped
|
|
22
|
+
|
|
23
|
+
onecli:
|
|
24
|
+
image: ghcr.io/onecli/onecli:latest
|
|
25
|
+
ports:
|
|
26
|
+
- "10254:10254" # Management API
|
|
27
|
+
- "10255:10255" # Credential proxy gateway
|
|
28
|
+
environment:
|
|
29
|
+
DATABASE_URL: postgresql://onecli:onecli@postgres:5432/onecli
|
|
30
|
+
APP_URL: http://0.0.0.0:10254
|
|
31
|
+
NEXT_PUBLIC_APP_URL: http://0.0.0.0:10254
|
|
32
|
+
AUTH_TRUST_HOST: "true"
|
|
33
|
+
NEXTAUTH_URL: http://localhost:10254
|
|
34
|
+
volumes:
|
|
35
|
+
- onecli_data:/app/data
|
|
36
|
+
networks:
|
|
37
|
+
- onecli_net
|
|
38
|
+
depends_on:
|
|
39
|
+
- postgres
|
|
40
|
+
restart: unless-stopped
|
|
41
|
+
healthcheck:
|
|
42
|
+
test: ["CMD-SHELL", "wget -qO- http://localhost:10254/api/health 2>/dev/null || wget -qO- http://localhost:10254 2>/dev/null | grep -q . && echo ok"]
|
|
43
|
+
interval: 5s
|
|
44
|
+
timeout: 5s
|
|
45
|
+
retries: 30
|
|
46
|
+
start_period: 10s
|
|
47
|
+
|
|
48
|
+
volumes:
|
|
49
|
+
onecli_postgres:
|
|
50
|
+
onecli_data:
|
|
51
|
+
|
|
52
|
+
networks:
|
|
53
|
+
onecli_net:
|
|
54
|
+
name: onecli_onecli
|