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,142 @@
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."
@@ -0,0 +1,13 @@
1
+ // Channel self-registration barrel file.
2
+ // Each import triggers the channel module's registerChannel() call.
3
+
4
+ // discord
5
+
6
+ // gmail
7
+
8
+ // slack
9
+ import './slack.js';
10
+
11
+ // telegram
12
+
13
+ // whatsapp
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ import {
4
+ registerChannel,
5
+ getChannelFactory,
6
+ getRegisteredChannelNames,
7
+ } from './registry.js';
8
+
9
+ // The registry is module-level state, so we need a fresh module per test.
10
+ // We use dynamic import with cache-busting to isolate tests.
11
+ // However, since vitest runs each file in its own context and we control
12
+ // registration order, we can test the public API directly.
13
+
14
+ describe('channel registry', () => {
15
+ // Note: registry is shared module state across tests in this file.
16
+ // Tests are ordered to account for cumulative registrations.
17
+
18
+ it('getChannelFactory returns undefined for unknown channel', () => {
19
+ expect(getChannelFactory('nonexistent')).toBeUndefined();
20
+ });
21
+
22
+ it('registerChannel and getChannelFactory round-trip', () => {
23
+ const factory = () => null;
24
+ registerChannel('test-channel', factory);
25
+ expect(getChannelFactory('test-channel')).toBe(factory);
26
+ });
27
+
28
+ it('getRegisteredChannelNames includes registered channels', () => {
29
+ registerChannel('another-channel', () => null);
30
+ const names = getRegisteredChannelNames();
31
+ expect(names).toContain('test-channel');
32
+ expect(names).toContain('another-channel');
33
+ });
34
+
35
+ it('later registration overwrites earlier one', () => {
36
+ const factory1 = () => null;
37
+ const factory2 = () => null;
38
+ registerChannel('overwrite-test', factory1);
39
+ registerChannel('overwrite-test', factory2);
40
+ expect(getChannelFactory('overwrite-test')).toBe(factory2);
41
+ });
42
+ });
@@ -0,0 +1,28 @@
1
+ import {
2
+ Channel,
3
+ OnInboundMessage,
4
+ OnChatMetadata,
5
+ RegisteredGroup,
6
+ } from '../types.js';
7
+
8
+ export interface ChannelOpts {
9
+ onMessage: OnInboundMessage;
10
+ onChatMetadata: OnChatMetadata;
11
+ registeredGroups: () => Record<string, RegisteredGroup>;
12
+ }
13
+
14
+ export type ChannelFactory = (opts: ChannelOpts) => Channel | null;
15
+
16
+ const registry = new Map<string, ChannelFactory>();
17
+
18
+ export function registerChannel(name: string, factory: ChannelFactory): void {
19
+ registry.set(name, factory);
20
+ }
21
+
22
+ export function getChannelFactory(name: string): ChannelFactory | undefined {
23
+ return registry.get(name);
24
+ }
25
+
26
+ export function getRegisteredChannelNames(): string[] {
27
+ return [...registry.keys()];
28
+ }