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.
Files changed (80) hide show
  1. package/README.md +101 -0
  2. package/bin/create.js +394 -0
  3. package/package.json +33 -0
  4. package/template/.env.example +38 -0
  5. package/template/CLAUDE.md +104 -0
  6. package/template/agent-credentials.yaml +33 -0
  7. package/template/agents.yaml +22 -0
  8. package/template/container/Dockerfile +70 -0
  9. package/template/container/Dockerfile.argus +34 -0
  10. package/template/container/agent-runner/package-lock.json +1524 -0
  11. package/template/container/agent-runner/package.json +23 -0
  12. package/template/container/agent-runner/src/index.ts +630 -0
  13. package/template/container/agent-runner/src/ipc-mcp-stdio.ts +339 -0
  14. package/template/container/agent-runner/tsconfig.json +15 -0
  15. package/template/container/build-argus.sh +25 -0
  16. package/template/container/build.sh +23 -0
  17. package/template/container/skills/agent-browser/SKILL.md +159 -0
  18. package/template/container/skills/agent-status/SKILL.md +69 -0
  19. package/template/container/skills/capabilities/SKILL.md +100 -0
  20. package/template/container/skills/edit-agent/SKILL.md +93 -0
  21. package/template/container/skills/slack-formatting/SKILL.md +92 -0
  22. package/template/container/skills/status/SKILL.md +104 -0
  23. package/template/container/tools/elastic_query.py +161 -0
  24. package/template/container/tools/gdrive_tool.py +185 -0
  25. package/template/container/tools/jira_tool.py +433 -0
  26. package/template/container/tools/slack_history_tool.py +144 -0
  27. package/template/container/tools/youtube_tool.py +174 -0
  28. package/template/docker-compose.yml +54 -0
  29. package/template/docs/how-it-works.md +496 -0
  30. package/template/eslint.config.js +32 -0
  31. package/template/groups/forge/CLAUDE.md +107 -0
  32. package/template/package-lock.json +5278 -0
  33. package/template/package.json +52 -0
  34. package/template/scripts/github-app-token.py +58 -0
  35. package/template/scripts/register-expense-agent.sh +121 -0
  36. package/template/scripts/run-migrations.ts +105 -0
  37. package/template/scripts/setup-onecli-secrets.sh +252 -0
  38. package/template/setup-agents.sh +142 -0
  39. package/template/src/channels/index.ts +13 -0
  40. package/template/src/channels/registry.test.ts +42 -0
  41. package/template/src/channels/registry.ts +28 -0
  42. package/template/src/channels/slack.test.ts +859 -0
  43. package/template/src/channels/slack.ts +373 -0
  44. package/template/src/claw-skill.test.ts +45 -0
  45. package/template/src/config.ts +94 -0
  46. package/template/src/container-runner.test.ts +221 -0
  47. package/template/src/container-runner.ts +1029 -0
  48. package/template/src/container-runtime.test.ts +149 -0
  49. package/template/src/container-runtime.ts +124 -0
  50. package/template/src/db-migration.test.ts +67 -0
  51. package/template/src/db.test.ts +484 -0
  52. package/template/src/db.ts +837 -0
  53. package/template/src/env.ts +42 -0
  54. package/template/src/formatting.test.ts +294 -0
  55. package/template/src/github-token.ts +48 -0
  56. package/template/src/google-token.ts +75 -0
  57. package/template/src/group-folder.test.ts +43 -0
  58. package/template/src/group-folder.ts +44 -0
  59. package/template/src/group-queue.test.ts +484 -0
  60. package/template/src/group-queue.ts +363 -0
  61. package/template/src/http-server.ts +343 -0
  62. package/template/src/index.ts +960 -0
  63. package/template/src/ipc-auth.test.ts +679 -0
  64. package/template/src/ipc.ts +548 -0
  65. package/template/src/logger.ts +16 -0
  66. package/template/src/mount-security.ts +421 -0
  67. package/template/src/network-policy.ts +119 -0
  68. package/template/src/remote-control.test.ts +397 -0
  69. package/template/src/remote-control.ts +224 -0
  70. package/template/src/router.ts +52 -0
  71. package/template/src/routing.test.ts +170 -0
  72. package/template/src/sender-allowlist.test.ts +216 -0
  73. package/template/src/sender-allowlist.ts +128 -0
  74. package/template/src/task-scheduler.test.ts +129 -0
  75. package/template/src/task-scheduler.ts +290 -0
  76. package/template/src/timezone.test.ts +73 -0
  77. package/template/src/timezone.ts +37 -0
  78. package/template/src/types.ts +114 -0
  79. package/template/src/worktree.ts +206 -0
  80. 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