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
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# IronClaws Kit
|
|
2
|
+
|
|
3
|
+
Scaffold an IronClaws AI agent platform in under 5 minutes.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx create-ironclaws my-workspace
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The wizard walks you through Slack tokens, LiteLLM gateway, and your first agent's channel. It then starts OneCLI, registers credentials, builds the container image, and optionally starts IronClaws. Send `@Forge hello` and your guide agent responds.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## What you get
|
|
14
|
+
|
|
15
|
+
Your first agent is **Forge** — a built-in guide that knows how IronClaws works and can help you build new agents directly from Slack. Ask it anything:
|
|
16
|
+
|
|
17
|
+
> `@Forge how do I add a new agent?`
|
|
18
|
+
> `@Forge explain how credentials work`
|
|
19
|
+
> `@Forge build me an agent that monitors Jira`
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Adding more agents
|
|
24
|
+
|
|
25
|
+
### Via Forge (recommended)
|
|
26
|
+
Just describe what you want in Slack. Forge will draft the CLAUDE.md, show you the `agents.yaml` and `.env` entries, and tell you to restart.
|
|
27
|
+
|
|
28
|
+
### Manually
|
|
29
|
+
|
|
30
|
+
**1. Write the agent's identity** — create `groups/my-agent/CLAUDE.md`:
|
|
31
|
+
```markdown
|
|
32
|
+
# My Agent
|
|
33
|
+
|
|
34
|
+
What this agent does in one sentence.
|
|
35
|
+
|
|
36
|
+
## What you do
|
|
37
|
+
Responsibilities.
|
|
38
|
+
|
|
39
|
+
## How to respond
|
|
40
|
+
Tone, format, constraints.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**2. Register it** — add to `agents.yaml`:
|
|
44
|
+
```yaml
|
|
45
|
+
- folder: my-agent
|
|
46
|
+
name: "My Agent"
|
|
47
|
+
trigger: "@Argus"
|
|
48
|
+
channel_env: MY_AGENT_CHANNEL_ID
|
|
49
|
+
requires_trigger: false
|
|
50
|
+
onecli_secrets: [litellm]
|
|
51
|
+
onecli_id: <python3 -c "import uuid; print(uuid.uuid4())">
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**3. Set the channel** — add to `.env`:
|
|
55
|
+
```
|
|
56
|
+
MY_AGENT_CHANNEL_ID=C...
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**4. Restart IronClaws** — the agent auto-registers, its channel is added to the allowlist, and it's live.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Giving agents tools and credentials
|
|
64
|
+
|
|
65
|
+
Edit `agent-credentials.yaml` to control what each agent can access:
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
agents:
|
|
69
|
+
my-agent:
|
|
70
|
+
skills: []
|
|
71
|
+
tools: [jira_tool.py] # Python tools from container/tools/
|
|
72
|
+
config:
|
|
73
|
+
- JIRA_BASE_URL # non-secret config from .env
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
For service credentials (Jira, Slack, etc.) add them to `onecli_secrets` in `agents.yaml`. OneCLI injects them as Authorization headers — agents never see the raw keys.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Requirements
|
|
81
|
+
|
|
82
|
+
- Node.js 18+
|
|
83
|
+
- Docker
|
|
84
|
+
- A Slack app with Socket Mode and a bot token
|
|
85
|
+
- A LiteLLM gateway (or Anthropic API access)
|
|
86
|
+
|
|
87
|
+
## Manual setup (without the wizard)
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
git clone https://github.com/ardoq/ironclaws-kit my-workspace
|
|
91
|
+
cd my-workspace
|
|
92
|
+
cp .env.example .env # fill in your values
|
|
93
|
+
docker compose up -d # start OneCLI infrastructure
|
|
94
|
+
npm ci
|
|
95
|
+
cd container && ./build.sh && ./build-argus.sh && cd ..
|
|
96
|
+
npm run dev
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Production note
|
|
100
|
+
|
|
101
|
+
Network enforcement (iptables) is Linux-only. On macOS/Windows dev machines, agents have unrestricted outbound access. Deploy on Linux for production.
|
package/bin/create.js
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* create-ironclaws — Interactive setup wizard for IronClaws
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx create-ironclaws [project-name]
|
|
7
|
+
* npx create-ironclaws@latest my-it-ops
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import prompts from 'prompts';
|
|
11
|
+
import kleur from 'kleur';
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { execSync, spawnSync } from 'child_process';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const TEMPLATE_DIR = path.join(__dirname, '..', 'template');
|
|
19
|
+
|
|
20
|
+
// ── Utilities ──────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
function log(msg) { console.log(msg); }
|
|
23
|
+
function success(msg) { console.log(kleur.green(' ✓ ') + msg); }
|
|
24
|
+
function info(msg) { console.log(kleur.cyan(' → ') + msg); }
|
|
25
|
+
function warn(msg) { console.log(kleur.yellow(' ⚠ ') + msg); }
|
|
26
|
+
function fatal(msg) { console.error(kleur.red(' ✗ ') + msg); process.exit(1); }
|
|
27
|
+
|
|
28
|
+
function hasCommand(cmd) {
|
|
29
|
+
try { execSync(`which ${cmd}`, { stdio: 'pipe' }); return true; } catch { return false; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function copyDir(src, dest) {
|
|
33
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
34
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
35
|
+
const srcPath = path.join(src, entry.name);
|
|
36
|
+
const destPath = path.join(dest, entry.name);
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
copyDir(srcPath, destPath);
|
|
39
|
+
} else {
|
|
40
|
+
fs.copyFileSync(srcPath, destPath);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function waitForOneCLI(maxWaitMs = 60000) {
|
|
46
|
+
const start = Date.now();
|
|
47
|
+
info('Waiting for OneCLI to be ready...');
|
|
48
|
+
while (Date.now() - start < maxWaitMs) {
|
|
49
|
+
try {
|
|
50
|
+
execSync('curl -sf http://localhost:10254/api/secrets > /dev/null 2>&1', { stdio: 'pipe' });
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
execSync('sleep 2', { stdio: 'pipe' });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function registerOneCLISecret(name, hostPattern, headerName, value, apiKey) {
|
|
60
|
+
const authHeader = apiKey ? `-H "Authorization: Bearer ${apiKey}"` : '';
|
|
61
|
+
|
|
62
|
+
// Delete existing
|
|
63
|
+
try {
|
|
64
|
+
const existing = execSync(
|
|
65
|
+
`curl -sf ${authHeader} http://localhost:10254/api/secrets`,
|
|
66
|
+
{ encoding: 'utf-8', stdio: 'pipe' }
|
|
67
|
+
);
|
|
68
|
+
const secrets = JSON.parse(existing);
|
|
69
|
+
const match = secrets.find(s => s.name === name);
|
|
70
|
+
if (match) {
|
|
71
|
+
execSync(`curl -sf -X DELETE ${authHeader} http://localhost:10254/api/secrets/${match.id}`, { stdio: 'pipe' });
|
|
72
|
+
}
|
|
73
|
+
} catch { /* ok */ }
|
|
74
|
+
|
|
75
|
+
// Create
|
|
76
|
+
const body = JSON.stringify({
|
|
77
|
+
name,
|
|
78
|
+
type: 'generic',
|
|
79
|
+
value,
|
|
80
|
+
hostPattern,
|
|
81
|
+
injectionConfig: { headerName },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
execSync(
|
|
85
|
+
`curl -sf -X POST ${authHeader} http://localhost:10254/api/secrets -H "Content-Type: application/json" -d '${body}'`,
|
|
86
|
+
{ stdio: 'pipe' }
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Main ───────────────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
async function main() {
|
|
93
|
+
console.log();
|
|
94
|
+
console.log(kleur.bold().white(' ✦ IronClaws Setup'));
|
|
95
|
+
console.log(kleur.gray(' AI agents over Slack for IT operations'));
|
|
96
|
+
console.log();
|
|
97
|
+
|
|
98
|
+
// Check prerequisites
|
|
99
|
+
if (!hasCommand('node')) fatal('Node.js is required. Install from nodejs.org');
|
|
100
|
+
if (!hasCommand('docker')) fatal('Docker is required. Install from docs.docker.com');
|
|
101
|
+
|
|
102
|
+
// Project directory
|
|
103
|
+
const projectArg = process.argv[2];
|
|
104
|
+
let projectDir;
|
|
105
|
+
|
|
106
|
+
if (projectArg) {
|
|
107
|
+
projectDir = path.resolve(process.cwd(), projectArg);
|
|
108
|
+
} else {
|
|
109
|
+
const { dir } = await prompts({
|
|
110
|
+
type: 'text',
|
|
111
|
+
name: 'dir',
|
|
112
|
+
message: 'Project directory',
|
|
113
|
+
initial: './my-ironclaws',
|
|
114
|
+
});
|
|
115
|
+
if (!dir) process.exit(0);
|
|
116
|
+
projectDir = path.resolve(process.cwd(), dir);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (fs.existsSync(projectDir) && fs.readdirSync(projectDir).length > 0) {
|
|
120
|
+
const { ok } = await prompts({
|
|
121
|
+
type: 'confirm',
|
|
122
|
+
name: 'ok',
|
|
123
|
+
message: `${projectDir} already exists and is not empty. Continue?`,
|
|
124
|
+
initial: false,
|
|
125
|
+
});
|
|
126
|
+
if (!ok) process.exit(0);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log();
|
|
130
|
+
|
|
131
|
+
// Core config
|
|
132
|
+
const core = await prompts([
|
|
133
|
+
{
|
|
134
|
+
type: 'text',
|
|
135
|
+
name: 'assistantName',
|
|
136
|
+
message: 'What should your assistant be called?',
|
|
137
|
+
initial: 'Argus',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
type: 'text',
|
|
141
|
+
name: 'slackBotToken',
|
|
142
|
+
message: 'Slack Bot Token (xoxb-...)',
|
|
143
|
+
validate: v => v.startsWith('xoxb-') ? true : 'Must start with xoxb-',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
type: 'text',
|
|
147
|
+
name: 'slackAppToken',
|
|
148
|
+
message: 'Slack App Token (xapp-...)',
|
|
149
|
+
validate: v => v.startsWith('xapp-') ? true : 'Must start with xapp-',
|
|
150
|
+
},
|
|
151
|
+
], { onCancel: () => process.exit(0) });
|
|
152
|
+
|
|
153
|
+
console.log();
|
|
154
|
+
log(kleur.gray(' LiteLLM gateway (routes Claude API calls through your proxy)'));
|
|
155
|
+
|
|
156
|
+
const llm = await prompts([
|
|
157
|
+
{
|
|
158
|
+
type: 'text',
|
|
159
|
+
name: 'litellmUrl',
|
|
160
|
+
message: 'LiteLLM gateway URL',
|
|
161
|
+
initial: 'https://your-litellm-gateway.example.com',
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
type: 'text',
|
|
165
|
+
name: 'litellmToken',
|
|
166
|
+
message: 'LiteLLM auth token (sk-...)',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
type: 'text',
|
|
170
|
+
name: 'anthropicModel',
|
|
171
|
+
message: 'Model name (must match LiteLLM deployment)',
|
|
172
|
+
initial: 'anthropic-claude-opus-4-6',
|
|
173
|
+
},
|
|
174
|
+
], { onCancel: () => process.exit(0) });
|
|
175
|
+
|
|
176
|
+
// Forge — the built-in guide agent
|
|
177
|
+
console.log();
|
|
178
|
+
log(kleur.gray(' Forge is your built-in guide — it knows how IronClaws works and can'));
|
|
179
|
+
log(kleur.gray(' help you build new agents directly from Slack (@Forge add a new agent).'));
|
|
180
|
+
console.log();
|
|
181
|
+
|
|
182
|
+
const agent = await prompts([
|
|
183
|
+
{
|
|
184
|
+
type: 'text',
|
|
185
|
+
name: 'agentName',
|
|
186
|
+
message: 'Forge display name (shown in Slack)',
|
|
187
|
+
initial: 'Forge',
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
type: 'text',
|
|
191
|
+
name: 'agentFolder',
|
|
192
|
+
message: 'Forge folder name',
|
|
193
|
+
initial: 'forge',
|
|
194
|
+
validate: v => /^[a-z0-9-]+$/.test(v) ? true : 'Lowercase letters, numbers and hyphens only',
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
type: 'text',
|
|
198
|
+
name: 'channelId',
|
|
199
|
+
message: 'Slack channel ID for Forge (right-click channel → View channel details)',
|
|
200
|
+
validate: v => v.startsWith('C') || v.startsWith('D') ? true : 'Should start with C (channel) or D (DM)',
|
|
201
|
+
},
|
|
202
|
+
], { onCancel: () => process.exit(0) });
|
|
203
|
+
|
|
204
|
+
// Optional
|
|
205
|
+
console.log();
|
|
206
|
+
const optional = await prompts([
|
|
207
|
+
{
|
|
208
|
+
type: 'confirm',
|
|
209
|
+
name: 'startInfra',
|
|
210
|
+
message: 'Start OneCLI infrastructure now? (docker compose up -d)',
|
|
211
|
+
initial: true,
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
type: 'confirm',
|
|
215
|
+
name: 'buildImage',
|
|
216
|
+
message: 'Build the agent container image? (takes ~2 min)',
|
|
217
|
+
initial: true,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
type: 'confirm',
|
|
221
|
+
name: 'startNano',
|
|
222
|
+
message: 'Start NanoClaw now?',
|
|
223
|
+
initial: true,
|
|
224
|
+
},
|
|
225
|
+
], { onCancel: () => process.exit(0) });
|
|
226
|
+
|
|
227
|
+
// ── Generate project ─────────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
console.log();
|
|
230
|
+
log(kleur.bold(' Creating your project...'));
|
|
231
|
+
console.log();
|
|
232
|
+
|
|
233
|
+
// Copy template
|
|
234
|
+
copyDir(TEMPLATE_DIR, projectDir);
|
|
235
|
+
success('Template copied');
|
|
236
|
+
|
|
237
|
+
// Rename forge folder to user's chosen name
|
|
238
|
+
const exampleDir = path.join(projectDir, 'groups', 'forge');
|
|
239
|
+
const agentDir = path.join(projectDir, 'groups', agent.agentFolder);
|
|
240
|
+
if (fs.existsSync(exampleDir)) {
|
|
241
|
+
fs.renameSync(exampleDir, agentDir);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Generate .env
|
|
245
|
+
const envContent = [
|
|
246
|
+
`# IronClaws — generated by create-ironclaws`,
|
|
247
|
+
``,
|
|
248
|
+
`# ── NanoClaw core ─────────────────────────────────────────────────────────────`,
|
|
249
|
+
`ASSISTANT_NAME=${core.assistantName}`,
|
|
250
|
+
`CONTAINER_IMAGE=argus-claude:latest`,
|
|
251
|
+
``,
|
|
252
|
+
`# ── Slack ─────────────────────────────────────────────────────────────────────`,
|
|
253
|
+
`SLACK_BOT_TOKEN=${core.slackBotToken}`,
|
|
254
|
+
`SLACK_APP_TOKEN=${core.slackAppToken}`,
|
|
255
|
+
``,
|
|
256
|
+
`# ── LiteLLM / Anthropic ───────────────────────────────────────────────────────`,
|
|
257
|
+
`ANTHROPIC_BASE_URL=${llm.litellmUrl}`,
|
|
258
|
+
`ANTHROPIC_BEDROCK_BASE_URL=${llm.litellmUrl}/bedrock`,
|
|
259
|
+
`ANTHROPIC_AUTH_TOKEN=${llm.litellmToken}`,
|
|
260
|
+
``,
|
|
261
|
+
`CLAUDE_CODE_USE_BEDROCK=1`,
|
|
262
|
+
`CLAUDE_CODE_SKIP_BEDROCK_AUTH=1`,
|
|
263
|
+
`CLAUDE_CODE_API_KEY_HELPER_TTL_MS=3600000`,
|
|
264
|
+
``,
|
|
265
|
+
`ANTHROPIC_MODEL=${llm.anthropicModel}`,
|
|
266
|
+
`ANTHROPIC_SMALL_FAST_MODEL=${llm.anthropicModel}`,
|
|
267
|
+
`ANTHROPIC_DEFAULT_OPUS_MODEL=${llm.anthropicModel}`,
|
|
268
|
+
`ANTHROPIC_DEFAULT_SONNET_MODEL=${llm.anthropicModel}`,
|
|
269
|
+
`ANTHROPIC_DEFAULT_HAIKU_MODEL=${llm.anthropicModel}`,
|
|
270
|
+
``,
|
|
271
|
+
`# ── Agent channel IDs ─────────────────────────────────────────────────────────`,
|
|
272
|
+
`${agent.agentFolder.toUpperCase().replace(/-/g, '_')}_CHANNEL_ID=${agent.channelId}`,
|
|
273
|
+
].join('\n');
|
|
274
|
+
|
|
275
|
+
fs.writeFileSync(path.join(projectDir, '.env'), envContent);
|
|
276
|
+
success('.env generated');
|
|
277
|
+
|
|
278
|
+
// Generate agents.yaml
|
|
279
|
+
const agentEnvKey = `${agent.agentFolder.toUpperCase().replace(/-/g, '_')}_CHANNEL_ID`;
|
|
280
|
+
const agentsYaml = [
|
|
281
|
+
`# agents.yaml — Your agent fleet`,
|
|
282
|
+
`# Add more agents here. Set their channel ID in .env.`,
|
|
283
|
+
`# NanoClaw auto-registers agents on startup.`,
|
|
284
|
+
``,
|
|
285
|
+
`agents:`,
|
|
286
|
+
` - folder: ${agent.agentFolder}`,
|
|
287
|
+
` name: "${agent.agentName}"`,
|
|
288
|
+
` trigger: "@${core.assistantName}"`,
|
|
289
|
+
` channel_env: ${agentEnvKey}`,
|
|
290
|
+
` requires_trigger: false`,
|
|
291
|
+
` onecli_secrets: [litellm]`,
|
|
292
|
+
` onecli_id: ${generateUUID()}`,
|
|
293
|
+
].join('\n');
|
|
294
|
+
|
|
295
|
+
fs.writeFileSync(path.join(projectDir, 'agents.yaml'), agentsYaml);
|
|
296
|
+
|
|
297
|
+
// Update agent-credentials.yaml with actual agent name
|
|
298
|
+
const credPath = path.join(projectDir, 'agent-credentials.yaml');
|
|
299
|
+
if (fs.existsSync(credPath)) {
|
|
300
|
+
const cred = fs.readFileSync(credPath, 'utf-8')
|
|
301
|
+
.replace('forge:', `${agent.agentFolder}:`);
|
|
302
|
+
fs.writeFileSync(credPath, cred);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
success('Configuration generated');
|
|
306
|
+
|
|
307
|
+
// Start infrastructure
|
|
308
|
+
if (optional.startInfra) {
|
|
309
|
+
console.log();
|
|
310
|
+
info('Starting OneCLI infrastructure...');
|
|
311
|
+
const result = spawnSync('docker', ['compose', 'up', '-d'], {
|
|
312
|
+
cwd: projectDir,
|
|
313
|
+
stdio: 'inherit',
|
|
314
|
+
});
|
|
315
|
+
if (result.status !== 0) {
|
|
316
|
+
warn('docker compose up failed — start it manually: docker compose up -d');
|
|
317
|
+
} else {
|
|
318
|
+
success('OneCLI infrastructure started');
|
|
319
|
+
|
|
320
|
+
// Register LiteLLM secret
|
|
321
|
+
if (llm.litellmToken && llm.litellmUrl) {
|
|
322
|
+
info('Registering LiteLLM credential with OneCLI...');
|
|
323
|
+
const ready = waitForOneCLI();
|
|
324
|
+
if (ready) {
|
|
325
|
+
try {
|
|
326
|
+
const host = llm.litellmUrl.replace(/^https?:\/\//, '').split('/')[0];
|
|
327
|
+
registerOneCLISecret('litellm', host, 'Authorization', `Bearer ${llm.litellmToken}`, '');
|
|
328
|
+
success(`LiteLLM secret registered for ${host}`);
|
|
329
|
+
} catch (e) {
|
|
330
|
+
warn(`Could not register secret automatically. Run manually: bash ${path.join(projectDir, 'scripts/setup-onecli-secrets.sh')}`);
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
warn('OneCLI not ready in time — run setup-onecli-secrets.sh manually after it starts');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Install dependencies
|
|
340
|
+
console.log();
|
|
341
|
+
info('Installing dependencies...');
|
|
342
|
+
spawnSync('npm', ['ci'], { cwd: projectDir, stdio: 'inherit' });
|
|
343
|
+
success('Dependencies installed');
|
|
344
|
+
|
|
345
|
+
// Build container image
|
|
346
|
+
if (optional.buildImage) {
|
|
347
|
+
console.log();
|
|
348
|
+
info('Building container image (this takes ~2 minutes)...');
|
|
349
|
+
const b1 = spawnSync('bash', ['./container/build.sh'], { cwd: projectDir, stdio: 'inherit' });
|
|
350
|
+
const b2 = spawnSync('bash', ['./container/build-argus.sh'], { cwd: projectDir, stdio: 'inherit' });
|
|
351
|
+
if (b1.status === 0 && b2.status === 0) {
|
|
352
|
+
success('Container image built: argus-claude:latest');
|
|
353
|
+
} else {
|
|
354
|
+
warn('Image build failed — run: cd container && ./build.sh && ./build-argus.sh');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Done!
|
|
359
|
+
console.log();
|
|
360
|
+
console.log(kleur.bold().green(' ✦ Setup complete!'));
|
|
361
|
+
console.log();
|
|
362
|
+
|
|
363
|
+
if (optional.startNano) {
|
|
364
|
+
info(`Starting NanoClaw in ${projectDir}...`);
|
|
365
|
+
console.log();
|
|
366
|
+
spawnSync('npm', ['run', 'dev'], { cwd: projectDir, stdio: 'inherit' });
|
|
367
|
+
} else {
|
|
368
|
+
log(` To start NanoClaw:`);
|
|
369
|
+
log(kleur.cyan(` cd ${path.relative(process.cwd(), projectDir)} && npm run dev`));
|
|
370
|
+
console.log();
|
|
371
|
+
log(` Then send this in Slack:`);
|
|
372
|
+
log(kleur.cyan(` @${core.assistantName} hello`));
|
|
373
|
+
console.log();
|
|
374
|
+
if (!optional.startInfra) {
|
|
375
|
+
log(` Don't forget to start OneCLI first:`);
|
|
376
|
+
log(kleur.cyan(` cd ${path.relative(process.cwd(), projectDir)} && docker compose up -d`));
|
|
377
|
+
console.log();
|
|
378
|
+
}
|
|
379
|
+
log(kleur.gray(` ⚠ Network enforcement (iptables) is only active on Linux.`));
|
|
380
|
+
log(kleur.gray(` On macOS/Windows, agents have unrestricted network access.`));
|
|
381
|
+
log(kleur.gray(` Use a Linux VM for production deployments.`));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
console.log();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function generateUUID() {
|
|
388
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
389
|
+
const r = Math.random() * 16 | 0;
|
|
390
|
+
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
main().catch(e => { console.error(e); process.exit(1); });
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-ironclaws",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Create an IronClaws AI agent platform in seconds",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-ironclaws": "bin/create.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"template"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node bin/create.js --dry-run"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"kleur": "^4.1.5",
|
|
18
|
+
"prompts": "^2.4.2"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"ai",
|
|
25
|
+
"agents",
|
|
26
|
+
"slack",
|
|
27
|
+
"it-ops",
|
|
28
|
+
"claude",
|
|
29
|
+
"nanoclaw",
|
|
30
|
+
"ironclaws"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT"
|
|
33
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# IronClaws configuration
|
|
2
|
+
# Generated by: npx create-ironclaws
|
|
3
|
+
# Fill in all values, then run: docker compose up -d && npm run dev
|
|
4
|
+
|
|
5
|
+
# ── NanoClaw core ─────────────────────────────────────────────────────────────
|
|
6
|
+
ASSISTANT_NAME=Argus
|
|
7
|
+
CONTAINER_IMAGE=argus-claude:latest
|
|
8
|
+
|
|
9
|
+
# ── Slack ─────────────────────────────────────────────────────────────────────
|
|
10
|
+
SLACK_BOT_TOKEN=xoxb-...
|
|
11
|
+
SLACK_APP_TOKEN=xapp-...
|
|
12
|
+
|
|
13
|
+
# ── LiteLLM / Anthropic ───────────────────────────────────────────────────────
|
|
14
|
+
ANTHROPIC_BASE_URL=https://your-litellm-gateway.example.com
|
|
15
|
+
ANTHROPIC_BEDROCK_BASE_URL=https://your-litellm-gateway.example.com/bedrock
|
|
16
|
+
ANTHROPIC_AUTH_TOKEN=sk-...
|
|
17
|
+
|
|
18
|
+
CLAUDE_CODE_USE_BEDROCK=1
|
|
19
|
+
CLAUDE_CODE_SKIP_BEDROCK_AUTH=1
|
|
20
|
+
CLAUDE_CODE_API_KEY_HELPER_TTL_MS=3600000
|
|
21
|
+
|
|
22
|
+
ANTHROPIC_MODEL=anthropic-claude-opus-4-6
|
|
23
|
+
ANTHROPIC_SMALL_FAST_MODEL=anthropic-claude-opus-4-6
|
|
24
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL=anthropic-claude-opus-4-6
|
|
25
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL=anthropic-claude-sonnet-4-6
|
|
26
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL=anthropic.claude-haiku-4-5
|
|
27
|
+
|
|
28
|
+
# ── GitHub (optional — needed for agents that create PRs) ─────────────────────
|
|
29
|
+
# GITHUB_APP_ID=
|
|
30
|
+
# GITHUB_APP_INSTALLATION_ID=
|
|
31
|
+
# GITHUB_REPO=owner/repo
|
|
32
|
+
|
|
33
|
+
# ── OneCLI API key (if your OneCLI instance has auth enabled) ─────────────────
|
|
34
|
+
# ONECLI_API_KEY=oc_...
|
|
35
|
+
|
|
36
|
+
# ── Agent channel IDs ─────────────────────────────────────────────────────────
|
|
37
|
+
# Right-click a channel in Slack → View channel details → scroll to bottom
|
|
38
|
+
FORGE_CHANNEL_ID=C... # Forge — your IronClaws guide agent
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# NanoClaw — Ardoq IT Ops Agent Platform
|
|
2
|
+
|
|
3
|
+
Internal deployment of NanoClaw. Hosts the Ardoq IT agent fleet over Slack.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
Single Node.js host process. Each registered agent gets its own Docker container (isolated filesystem + memory) spawned on demand. Agents communicate back via IPC files. All agents share one Docker image: `argus-claude:latest`.
|
|
8
|
+
|
|
9
|
+
## Key Files
|
|
10
|
+
|
|
11
|
+
| File | Purpose |
|
|
12
|
+
|------|---------|
|
|
13
|
+
| `src/index.ts` | Main loop: message routing, agent invocation, state |
|
|
14
|
+
| `src/channels/slack.ts` | Slack channel integration (Socket Mode) |
|
|
15
|
+
| `src/container-runner.ts` | Spawns containers with correct mounts and env vars |
|
|
16
|
+
| `src/ipc.ts` | IPC file watcher — agents write here to send messages / schedule tasks |
|
|
17
|
+
| `src/task-scheduler.ts` | Cron-based task scheduling |
|
|
18
|
+
| `src/db.ts` | SQLite: messages, groups, sessions, tasks |
|
|
19
|
+
| `src/config.ts` | Trigger patterns, paths, env var loading |
|
|
20
|
+
| `src/sender-allowlist.ts` | Per-channel sender filtering |
|
|
21
|
+
| `groups/{name}/CLAUDE.md` | Per-agent identity and instructions |
|
|
22
|
+
| `container/Dockerfile.argus` | Agent container image definition |
|
|
23
|
+
| `container/tools/` | Python tools available inside containers |
|
|
24
|
+
| `container/skills/` | Shared skills available to all agents |
|
|
25
|
+
|
|
26
|
+
## Agent Fleet
|
|
27
|
+
|
|
28
|
+
| Agent | Folder | Channel | Purpose |
|
|
29
|
+
|-------|--------|---------|---------|
|
|
30
|
+
| Argus Alert | `argus-alert` | Alert channel | Monitors alerts, investigates incidents, raises fix PRs |
|
|
31
|
+
| Argus Deps | `argus-deps` | Deps channel | Manages Dependabot alerts, bumps packages |
|
|
32
|
+
| Byte | `byte` | IT standup | Team standup agent, team memory |
|
|
33
|
+
| Byte IT Internal | `byte-it-internal` | IT internal | Silent reader, writes to Byte's MEMORY.md |
|
|
34
|
+
| Ticket Creator | `ticket-creator` | Ticket channel | Creates Jira tickets from Slack conversation |
|
|
35
|
+
| Jira Worker | `jira-worker` | Worker channel | Picks up IAI tickets, implements, raises PRs |
|
|
36
|
+
| Global Claw | `global-claw` | DM (Sewnet only) | Meta-agent: manages all other agents |
|
|
37
|
+
|
|
38
|
+
## Container Image
|
|
39
|
+
|
|
40
|
+
All agents use `argus-claude:latest`. Built with:
|
|
41
|
+
```bash
|
|
42
|
+
cd container && ./build-argus.sh
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Set `CONTAINER_IMAGE=argus-claude:latest` in `.env`.
|
|
46
|
+
|
|
47
|
+
## Tools Available Inside Containers
|
|
48
|
+
|
|
49
|
+
Located in `container/tools/` — baked into the image:
|
|
50
|
+
- `elastic_query.py` — Kibana/Elastic log querying
|
|
51
|
+
- `jira_tool.py` — Jira CRUD (get, search, create, update, comment, assign)
|
|
52
|
+
- `gdrive_tool.py` — Google Drive transcript reading
|
|
53
|
+
- `slack_history_tool.py` — Slack channel history fetching
|
|
54
|
+
|
|
55
|
+
## Skills Available Inside Containers
|
|
56
|
+
|
|
57
|
+
Located in `container/skills/` — loaded at runtime:
|
|
58
|
+
- `agent-browser` — web browsing
|
|
59
|
+
- `capabilities` — list available skills
|
|
60
|
+
- `slack-formatting` — Slack mrkdwn formatting reference
|
|
61
|
+
- `status` — system health check
|
|
62
|
+
- `agent-status` — dashboard of all agents
|
|
63
|
+
- `edit-agent` — collaborative CLAUDE.md editing
|
|
64
|
+
|
|
65
|
+
## Runtime Config
|
|
66
|
+
|
|
67
|
+
Key env vars (set in `.env`):
|
|
68
|
+
```
|
|
69
|
+
SLACK_BOT_TOKEN Slack bot token (xoxb-...)
|
|
70
|
+
SLACK_APP_TOKEN Slack app-level token (xapp-...)
|
|
71
|
+
GITHUB_TOKEN GitHub PAT for raising PRs
|
|
72
|
+
GITHUB_APP_ID GitHub App ID (for token generation)
|
|
73
|
+
ELASTIC_BASE_URL Kibana base URL
|
|
74
|
+
ELASTIC_API_KEY Kibana API key
|
|
75
|
+
JIRA_BASE_URL https://ardoqcom.atlassian.net
|
|
76
|
+
JIRA_EMAIL Jira account email
|
|
77
|
+
JIRA_API_TOKEN Jira API token
|
|
78
|
+
JIRA_PROJECT_KEY IAI
|
|
79
|
+
CONTAINER_IMAGE argus-claude:latest
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Security
|
|
83
|
+
|
|
84
|
+
- Mount allowlist: `~/.config/nanoclaw/mount-allowlist.json` — controls what host paths containers can access
|
|
85
|
+
- Sender allowlist: `~/.config/nanoclaw/sender-allowlist.json` — per-channel sender filtering, outside project root (tamper-proof)
|
|
86
|
+
- Global Claw DM is restricted to Sewnet only (U04S2VA1CU8) at allowlist level
|
|
87
|
+
- `github-app.pem` is gitignored — never commit
|
|
88
|
+
|
|
89
|
+
## Running
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm run dev # Development (hot reload)
|
|
93
|
+
npm run build # Compile TypeScript
|
|
94
|
+
npm start # Production
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## First-Time Setup
|
|
98
|
+
|
|
99
|
+
1. Copy `.env.example` to `.env` and fill in all values
|
|
100
|
+
2. Run `./setup-agents.sh` to register all agents in the SQLite database
|
|
101
|
+
3. Update `~/.config/nanoclaw/sender-allowlist.json` with channel IDs
|
|
102
|
+
4. Update `~/.config/nanoclaw/mount-allowlist.json` for Global Claw repo access
|
|
103
|
+
5. Build the container: `cd container && ./build-argus.sh`
|
|
104
|
+
6. Start: `npm run dev`
|