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,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,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
|
+
}
|