@swarp/cli 0.0.1
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/bin/swarp.mjs +183 -0
- package/package.json +45 -0
- package/proto/swarp/v1/swarp.proto +157 -0
- package/src/certs/generate.mjs +467 -0
- package/src/certs/generate.test.mjs +265 -0
- package/src/config.mjs +11 -0
- package/src/generator/audit.mjs +164 -0
- package/src/generator/generate.mjs +105 -0
- package/src/generator/schema.mjs +105 -0
- package/src/init/index.mjs +189 -0
- package/src/init/index.test.mjs +168 -0
- package/src/init/wizard.mjs +38 -0
- package/src/mcp-server/dispatch.mjs +118 -0
- package/src/mcp-server/index.mjs +259 -0
- package/src/runtimes/fly-sprites.mjs +74 -0
- package/src/runtimes/interface.mjs +20 -0
- package/src/skill/generate.mjs +141 -0
- package/src/templates/agent.yaml +132 -0
- package/src/templates/agent.yaml.example.hbs +137 -0
- package/src/templates/agent.yaml.hbs +40 -0
- package/src/templates/router.yaml.hbs +11 -0
- package/src/templates/workflow.yml +20 -0
- package/src/templates/workflow.yml.hbs +16 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import {
|
|
4
|
+
CallToolRequestSchema,
|
|
5
|
+
ListToolsRequestSchema,
|
|
6
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import { DispatchClient } from './dispatch.mjs';
|
|
8
|
+
import { loadConfig } from '../config.mjs';
|
|
9
|
+
import { auditConfigs } from '../generator/audit.mjs';
|
|
10
|
+
import { generateRunnerConfig } from '../generator/generate.mjs';
|
|
11
|
+
|
|
12
|
+
export async function startMcpServer() {
|
|
13
|
+
const config = loadConfig('.swarp.json');
|
|
14
|
+
const client = new DispatchClient(config.router_url);
|
|
15
|
+
|
|
16
|
+
const server = new Server(
|
|
17
|
+
{ name: 'swarp', version: '1.0.0' },
|
|
18
|
+
{ capabilities: { tools: {} } },
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const agentTools = [];
|
|
22
|
+
let agentList = [];
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const { agents } = await client.listAgents();
|
|
26
|
+
agentList = agents ?? [];
|
|
27
|
+
for (const agent of agentList) {
|
|
28
|
+
agentTools.push(buildAgentTool(agent));
|
|
29
|
+
}
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error(`[swarp] Warning: could not reach router at ${config.router_url}: ${err.message}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const localTools = buildLocalTools(config);
|
|
35
|
+
|
|
36
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
37
|
+
return { tools: [...agentTools, ...localTools] };
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
41
|
+
const { name, arguments: toolArgs } = req.params;
|
|
42
|
+
|
|
43
|
+
const agent = agentList.find((a) => a.name === name);
|
|
44
|
+
if (agent) {
|
|
45
|
+
return handleAgentDispatch(client, agent.name, toolArgs);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (name === 'swarm_audit') return handleAudit(config, toolArgs);
|
|
49
|
+
if (name === 'swarm_generate') return handleGenerate(config, toolArgs);
|
|
50
|
+
if (name === 'swarm_status') return handleStatus(client, toolArgs);
|
|
51
|
+
if (name === 'swarm_deploy') return handleDeploy(config, toolArgs);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
55
|
+
isError: true,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const transport = new StdioServerTransport();
|
|
60
|
+
await server.connect(transport);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildAgentTool(agent) {
|
|
64
|
+
const modeNames = (agent.modes ?? []).map((m) => m.name);
|
|
65
|
+
return {
|
|
66
|
+
name: agent.name,
|
|
67
|
+
description: `Dispatch a task to the ${agent.name} agent (${agent.role ?? 'agent'}). Available modes: ${modeNames.join(', ') || 'any'}`,
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
mode: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
description: `Mode to run. One of: ${modeNames.join(', ') || 'any'}`,
|
|
74
|
+
},
|
|
75
|
+
params: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
description: 'Mode-specific parameters as key-value pairs',
|
|
78
|
+
additionalProperties: { type: 'string' },
|
|
79
|
+
},
|
|
80
|
+
session_id: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
description: 'Optional session ID to continue an existing conversation',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
required: ['mode'],
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildLocalTools(config) {
|
|
91
|
+
return [
|
|
92
|
+
{
|
|
93
|
+
name: 'swarm_audit',
|
|
94
|
+
description: 'Audit all agent.yaml files for schema errors and warnings',
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: {
|
|
98
|
+
agents_dir: {
|
|
99
|
+
type: 'string',
|
|
100
|
+
description: 'Directory containing agent subdirectories (default: config agents_dir)',
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'swarm_generate',
|
|
107
|
+
description: 'Generate runner configs from agent.yaml files',
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
agents_dir: {
|
|
112
|
+
type: 'string',
|
|
113
|
+
description: 'Directory containing agent subdirectories (default: config agents_dir)',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'swarm_status',
|
|
120
|
+
description: 'Get live status of an agent or all agents via gRPC',
|
|
121
|
+
inputSchema: {
|
|
122
|
+
type: 'object',
|
|
123
|
+
properties: {
|
|
124
|
+
agent: {
|
|
125
|
+
type: 'string',
|
|
126
|
+
description: 'Agent name (omit for all agents)',
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'swarm_deploy',
|
|
133
|
+
description: 'Deploy an agent sprite via the Sprites API. Requires SWARP_SPRITES_TOKEN.',
|
|
134
|
+
inputSchema: {
|
|
135
|
+
type: 'object',
|
|
136
|
+
properties: {
|
|
137
|
+
agent: {
|
|
138
|
+
type: 'string',
|
|
139
|
+
description: 'Agent name to deploy',
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
required: ['agent'],
|
|
143
|
+
},
|
|
144
|
+
annotations: {
|
|
145
|
+
destructiveHint: true,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function handleAgentDispatch(client, agentName, toolArgs) {
|
|
152
|
+
const { mode, params = {}, session_id: sessionId = '' } = toolArgs ?? {};
|
|
153
|
+
const events = [];
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
for await (const event of client.dispatchTask(
|
|
157
|
+
agentName,
|
|
158
|
+
{ structured: { mode, params } },
|
|
159
|
+
sessionId,
|
|
160
|
+
)) {
|
|
161
|
+
events.push(event);
|
|
162
|
+
}
|
|
163
|
+
} catch (err) {
|
|
164
|
+
return {
|
|
165
|
+
content: [{ type: 'text', text: `Dispatch error: ${err.message}` }],
|
|
166
|
+
isError: true,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const last = events.at(-1);
|
|
171
|
+
const text = last?.result?.summary ?? last?.text ?? '';
|
|
172
|
+
return { content: [{ type: 'text', text }] };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function handleAudit(config, toolArgs) {
|
|
176
|
+
const agentsDir = toolArgs?.agents_dir ?? config.agents_dir ?? 'agents';
|
|
177
|
+
try {
|
|
178
|
+
const results = await auditConfigs(agentsDir);
|
|
179
|
+
const lines = [];
|
|
180
|
+
for (const result of results) {
|
|
181
|
+
lines.push(`=== ${result.agent} ===`);
|
|
182
|
+
for (const c of result.checks) {
|
|
183
|
+
const icon = c.status === 'pass' ? '\u2713' : c.status === 'fail' ? '\u2717' : '\u25cb';
|
|
184
|
+
lines.push(` ${icon} [${c.severity.toUpperCase().slice(0, 3)}] ${c.name}: ${c.message}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
188
|
+
} catch (err) {
|
|
189
|
+
return { content: [{ type: 'text', text: `Audit error: ${err.message}` }], isError: true };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function handleGenerate(config, toolArgs) {
|
|
194
|
+
const agentsDir = toolArgs?.agents_dir ?? config.agents_dir ?? 'agents';
|
|
195
|
+
try {
|
|
196
|
+
await generateRunnerConfig(agentsDir);
|
|
197
|
+
return { content: [{ type: 'text', text: `Runner configs generated for agents in ${agentsDir}` }] };
|
|
198
|
+
} catch (err) {
|
|
199
|
+
return { content: [{ type: 'text', text: `Generate error: ${err.message}` }], isError: true };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function handleStatus(client, toolArgs) {
|
|
204
|
+
const { agent } = toolArgs ?? {};
|
|
205
|
+
try {
|
|
206
|
+
if (agent) {
|
|
207
|
+
const status = await client.getAgentStatus(agent);
|
|
208
|
+
return { content: [{ type: 'text', text: JSON.stringify(status, null, 2) }] };
|
|
209
|
+
}
|
|
210
|
+
const { agents } = await client.listAgents();
|
|
211
|
+
const lines = (agents ?? []).map(
|
|
212
|
+
(a) => `${a.name} (${a.online ? 'online' : 'offline'}) — ${a.modes?.length ?? 0} mode(s)`,
|
|
213
|
+
);
|
|
214
|
+
return { content: [{ type: 'text', text: lines.join('\n') || 'No agents registered' }] };
|
|
215
|
+
} catch (err) {
|
|
216
|
+
return { content: [{ type: 'text', text: `Status error: ${err.message}` }], isError: true };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function handleDeploy(config, toolArgs) {
|
|
221
|
+
const token = process.env.SWARP_SPRITES_TOKEN;
|
|
222
|
+
if (!token) {
|
|
223
|
+
return {
|
|
224
|
+
content: [{ type: 'text', text: 'Error: SWARP_SPRITES_TOKEN env var is required for deploy' }],
|
|
225
|
+
isError: true,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const { agent: agentName } = toolArgs ?? {};
|
|
230
|
+
if (!agentName) {
|
|
231
|
+
return {
|
|
232
|
+
content: [{ type: 'text', text: 'Error: agent name is required' }],
|
|
233
|
+
isError: true,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const { readFileSync, existsSync } = await import('node:fs');
|
|
239
|
+
const { resolve, join } = await import('node:path');
|
|
240
|
+
const yaml = (await import('js-yaml')).default;
|
|
241
|
+
const { FlySpriteAdapter } = await import('../runtimes/fly-sprites.mjs');
|
|
242
|
+
|
|
243
|
+
const agentsDir = resolve(config.agents_dir ?? 'agents');
|
|
244
|
+
const yamlPath = join(agentsDir, agentName, 'agent.yaml');
|
|
245
|
+
if (!existsSync(yamlPath)) {
|
|
246
|
+
return {
|
|
247
|
+
content: [{ type: 'text', text: `Error: agent.yaml not found at ${yamlPath}` }],
|
|
248
|
+
isError: true,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const agentConfig = yaml.load(readFileSync(yamlPath, 'utf-8'));
|
|
253
|
+
const adapter = new FlySpriteAdapter(token);
|
|
254
|
+
await adapter.createAgent(agentName, agentConfig);
|
|
255
|
+
return { content: [{ type: 'text', text: `Deployed ${agentName}` }] };
|
|
256
|
+
} catch (err) {
|
|
257
|
+
return { content: [{ type: 'text', text: `Deploy error: ${err.message}` }], isError: true };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { RuntimeAdapter } from './interface.mjs';
|
|
2
|
+
|
|
3
|
+
const SPRITES_API_BASE = 'https://sprites.fly.io/v1';
|
|
4
|
+
|
|
5
|
+
export class FlySpriteAdapter extends RuntimeAdapter {
|
|
6
|
+
constructor(token) {
|
|
7
|
+
if (!token) {
|
|
8
|
+
throw new Error('SWARP_SPRITES_TOKEN is required — never load it from .env');
|
|
9
|
+
}
|
|
10
|
+
super(token);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async createAgent(name, config) {
|
|
14
|
+
const res = await fetch(`${SPRITES_API_BASE}/sprites`, {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: {
|
|
17
|
+
Authorization: `Bearer ${this.token}`,
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
},
|
|
20
|
+
body: JSON.stringify({
|
|
21
|
+
name,
|
|
22
|
+
config: {
|
|
23
|
+
name: config.name,
|
|
24
|
+
version: config.version,
|
|
25
|
+
grpc_port: config.grpc_port ?? 50052,
|
|
26
|
+
router_url: config.router_url,
|
|
27
|
+
region: config.region,
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
const body = await res.text();
|
|
34
|
+
throw new Error(`Sprites API error creating ${name}: ${res.status} ${body}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return res.json();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async destroyAgent(name) {
|
|
41
|
+
const res = await fetch(`${SPRITES_API_BASE}/sprites/${encodeURIComponent(name)}`, {
|
|
42
|
+
method: 'DELETE',
|
|
43
|
+
headers: {
|
|
44
|
+
Authorization: `Bearer ${this.token}`,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!res.ok && res.status !== 404) {
|
|
49
|
+
const body = await res.text();
|
|
50
|
+
throw new Error(`Sprites API error destroying ${name}: ${res.status} ${body}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { destroyed: true, name };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async getAgent(name) {
|
|
57
|
+
const res = await fetch(`${SPRITES_API_BASE}/sprites/${encodeURIComponent(name)}`, {
|
|
58
|
+
headers: {
|
|
59
|
+
Authorization: `Bearer ${this.token}`,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (res.status === 404) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
const body = await res.text();
|
|
69
|
+
throw new Error(`Sprites API error getting ${name}: ${res.status} ${body}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return res.json();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class RuntimeAdapter {
|
|
2
|
+
constructor(token) {
|
|
3
|
+
if (new.target === RuntimeAdapter) {
|
|
4
|
+
throw new Error('RuntimeAdapter is abstract — extend it with a concrete implementation');
|
|
5
|
+
}
|
|
6
|
+
this.token = token;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async createAgent(name, config) {
|
|
10
|
+
throw new Error('createAgent() must be implemented by subclass');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async destroyAgent(name) {
|
|
14
|
+
throw new Error('destroyAgent() must be implemented by subclass');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async getAgent(name) {
|
|
18
|
+
throw new Error('getAgent() must be implemented by subclass');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Scans the agents directory, reads each agent.yaml, and generates
|
|
7
|
+
* a SKILL.md tailored to the user's actual agents and modes.
|
|
8
|
+
*
|
|
9
|
+
* @param {object} opts
|
|
10
|
+
* @param {string} opts.agentsDir - Absolute path to agents directory
|
|
11
|
+
* @param {string} opts.routerUrl - Router gRPC address
|
|
12
|
+
* @returns {string} Rendered SKILL.md content
|
|
13
|
+
*/
|
|
14
|
+
export function generateSkill({ agentsDir, routerUrl }) {
|
|
15
|
+
const agents = scanAgents(agentsDir);
|
|
16
|
+
|
|
17
|
+
let md = `# /swarp
|
|
18
|
+
|
|
19
|
+
Use this skill when dispatching tasks to SWARP agents, checking agent status, or managing deployments.
|
|
20
|
+
|
|
21
|
+
## Available Agents
|
|
22
|
+
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
if (agents.length === 0) {
|
|
26
|
+
md += `No agents found in \`${agentsDir}\`. Run \`npx @swarp/cli init\` to create one.\n`;
|
|
27
|
+
} else {
|
|
28
|
+
for (const agent of agents) {
|
|
29
|
+
md += `### \`${agent.name}\`\n\n`;
|
|
30
|
+
if (agent.modes.length > 0) {
|
|
31
|
+
md += '| Mode | Description | Model | Timeout |\n';
|
|
32
|
+
md += '|------|-------------|-------|--------|\n';
|
|
33
|
+
for (const mode of agent.modes) {
|
|
34
|
+
const desc = mode.description || '—';
|
|
35
|
+
const model = mode.model || 'default';
|
|
36
|
+
const timeout = mode.timeout_minutes ? `${mode.timeout_minutes}m` : '30m';
|
|
37
|
+
md += `| \`${mode.name}\` | ${desc} | ${model} | ${timeout} |\n`;
|
|
38
|
+
}
|
|
39
|
+
md += '\n';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
md += `## MCP Tools
|
|
45
|
+
|
|
46
|
+
The SWARP MCP server registers one tool per agent, plus management tools.
|
|
47
|
+
|
|
48
|
+
### Agent tools (one per agent above)
|
|
49
|
+
|
|
50
|
+
Each agent tool accepts:
|
|
51
|
+
- \`mode\` (required): one of the modes listed above
|
|
52
|
+
- \`params\` (optional): key-value pairs interpolated into the mode prompt
|
|
53
|
+
- \`session_id\` (optional): resume an existing session
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
\`\`\`
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
if (agents.length > 0) {
|
|
60
|
+
const first = agents[0];
|
|
61
|
+
const firstMode = first.modes[0]?.name || 'implement';
|
|
62
|
+
md += `Use the \`${first.name}\` tool with mode "${firstMode}" and params.task = "Add error handling to the API"\n`;
|
|
63
|
+
} else {
|
|
64
|
+
md += `Use the \`agent-name\` tool with mode "implement" and params.task = "..."\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
md += `\`\`\`
|
|
68
|
+
|
|
69
|
+
### Management tools
|
|
70
|
+
|
|
71
|
+
| Tool | Description |
|
|
72
|
+
|------|-------------|
|
|
73
|
+
| \`swarm_status\` | Check if an agent is online |
|
|
74
|
+
| \`swarm_audit\` | Validate all agent.yaml files |
|
|
75
|
+
| \`swarm_generate\` | Regenerate runner configs |
|
|
76
|
+
| \`swarm_deploy\` | Deploy an agent (destructive — confirm first) |
|
|
77
|
+
|
|
78
|
+
## CLI
|
|
79
|
+
|
|
80
|
+
\`\`\`bash
|
|
81
|
+
npx @swarp/cli status # All agents
|
|
82
|
+
npx @swarp/cli status <agent> # Specific agent
|
|
83
|
+
npx @swarp/cli audit # Validate configs
|
|
84
|
+
npx @swarp/cli deploy <agent> # Deploy one agent
|
|
85
|
+
npx @swarp/cli deploy --all # Deploy all
|
|
86
|
+
npx @swarp/cli certs generate # Generate mTLS keypairs
|
|
87
|
+
\`\`\`
|
|
88
|
+
|
|
89
|
+
## Configuration
|
|
90
|
+
|
|
91
|
+
Router: \`${routerUrl}\`
|
|
92
|
+
Agents directory: \`${path.relative(process.cwd(), agentsDir) || agentsDir}\`
|
|
93
|
+
|
|
94
|
+
## Environment Variables
|
|
95
|
+
|
|
96
|
+
| Variable | Required | Description |
|
|
97
|
+
|----------|----------|-------------|
|
|
98
|
+
| \`SWARP_SPRITES_TOKEN\` | For deploy | Fly Sprites API token |
|
|
99
|
+
| \`SWARP_ROUTER_URL\` | For gRPC | Router address (overrides .swarp.json) |
|
|
100
|
+
|
|
101
|
+
**Security:** \`SWARP_SPRITES_TOKEN\` must be set in the environment — never loaded from .env files.
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
return md;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Reads all agent.yaml files from the agents directory.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} agentsDir
|
|
111
|
+
* @returns {Array<{name: string, modes: Array<{name: string, description?: string, model?: string, timeout_minutes?: number}>}>}
|
|
112
|
+
*/
|
|
113
|
+
function scanAgents(agentsDir) {
|
|
114
|
+
if (!fs.existsSync(agentsDir)) return [];
|
|
115
|
+
|
|
116
|
+
const entries = fs.readdirSync(agentsDir, { withFileTypes: true });
|
|
117
|
+
const agents = [];
|
|
118
|
+
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
if (!entry.isDirectory()) continue;
|
|
121
|
+
const yamlPath = path.join(agentsDir, entry.name, 'agent.yaml');
|
|
122
|
+
if (!fs.existsSync(yamlPath)) continue;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const config = yaml.load(fs.readFileSync(yamlPath, 'utf8'));
|
|
126
|
+
agents.push({
|
|
127
|
+
name: config.name || entry.name,
|
|
128
|
+
modes: (config.modes || []).map((m) => ({
|
|
129
|
+
name: m.name,
|
|
130
|
+
description: m.description,
|
|
131
|
+
model: m.model,
|
|
132
|
+
timeout_minutes: m.timeout_minutes,
|
|
133
|
+
})),
|
|
134
|
+
});
|
|
135
|
+
} catch {
|
|
136
|
+
// Skip malformed configs — swarm_audit will catch them
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return agents.sort((a, b) => a.name.localeCompare(b.name));
|
|
141
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# agents/example/agent.yaml
|
|
2
|
+
#
|
|
3
|
+
# SWARP agent configuration — all fields shown with explanations.
|
|
4
|
+
# Required fields: name, version, grpc_port, router_url, modes
|
|
5
|
+
# All other fields are optional.
|
|
6
|
+
|
|
7
|
+
# ── Identity ──────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
# Unique agent name. Used as the key in the router registry and as the MCP tool
|
|
10
|
+
# name exposed to Claude Code.
|
|
11
|
+
name: example
|
|
12
|
+
|
|
13
|
+
# Semantic version of this agent's configuration. The runner reports this
|
|
14
|
+
# version to the router on registration and includes it in AgentEvent metadata.
|
|
15
|
+
version: '1.0.0'
|
|
16
|
+
|
|
17
|
+
# ── Network ───────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
# gRPC port this runner listens on. Each agent on the same Fly Machine must
|
|
20
|
+
# use a unique port. The router connects to this port for Dispatch calls.
|
|
21
|
+
grpc_port: 50052
|
|
22
|
+
|
|
23
|
+
# Address of the SWARP router. Use an env var reference so the value is not
|
|
24
|
+
# hardcoded and can differ between local dev and production.
|
|
25
|
+
router_url: '${SWARP_ROUTER_URL}'
|
|
26
|
+
|
|
27
|
+
# ── Persona ───────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
# persona sets the character of this agent. It is prepended to every prompt
|
|
30
|
+
# as a system-level instruction before any mode-specific content.
|
|
31
|
+
persona: |
|
|
32
|
+
You are example, a focused software engineer.
|
|
33
|
+
You write clean, tested, idiomatic code.
|
|
34
|
+
You ask clarifying questions before starting large tasks.
|
|
35
|
+
You never make up facts.
|
|
36
|
+
|
|
37
|
+
# ── Preamble ──────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
# preamble is injected after the persona and before the user's task. Use it
|
|
40
|
+
# for project-specific conventions that every mode should know about.
|
|
41
|
+
preamble: |
|
|
42
|
+
Project: monorepo (Bun + React + Supabase)
|
|
43
|
+
Branch policy: never push to main; always use feature branches.
|
|
44
|
+
Test runner: bun test
|
|
45
|
+
Linter: bun run lint
|
|
46
|
+
|
|
47
|
+
# ── Modes ─────────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
# A mode is a named capability of this agent. The router's classifier selects
|
|
50
|
+
# a mode based on keywords in the incoming task description.
|
|
51
|
+
#
|
|
52
|
+
# Required per mode: name
|
|
53
|
+
# Optional: description, model, max_turns, timeout_minutes, tools, skills, hooks, env
|
|
54
|
+
modes:
|
|
55
|
+
- name: implement
|
|
56
|
+
description: 'Implement a feature or fix a bug from a spec'
|
|
57
|
+
# Model to use for this mode. Defaults to the runner's configured default.
|
|
58
|
+
model: claude-opus-4-5
|
|
59
|
+
# Maximum number of agentic turns before the runner halts the session.
|
|
60
|
+
max_turns: 40
|
|
61
|
+
# Session wall-clock timeout in minutes. The runner cancels the claude
|
|
62
|
+
# process when this expires.
|
|
63
|
+
timeout_minutes: 30
|
|
64
|
+
# tools lists MCP tool names this mode is allowed to call.
|
|
65
|
+
tools:
|
|
66
|
+
- Read
|
|
67
|
+
- Edit
|
|
68
|
+
- Write
|
|
69
|
+
- Bash
|
|
70
|
+
- Glob
|
|
71
|
+
- Grep
|
|
72
|
+
# skills lists slash-command skill names this mode loads on startup.
|
|
73
|
+
skills:
|
|
74
|
+
- superpowers:test-driven-development
|
|
75
|
+
- superpowers:systematic-debugging
|
|
76
|
+
# hooks are shell commands run before or after the session.
|
|
77
|
+
hooks:
|
|
78
|
+
pre_session:
|
|
79
|
+
- 'git fetch origin'
|
|
80
|
+
post_session:
|
|
81
|
+
- 'bun run lint'
|
|
82
|
+
- 'bun test'
|
|
83
|
+
# env injects key=value pairs into the runner's process environment.
|
|
84
|
+
# Secrets should use ${SECRET_NAME} references.
|
|
85
|
+
env:
|
|
86
|
+
NODE_ENV: development
|
|
87
|
+
|
|
88
|
+
- name: review
|
|
89
|
+
description: 'Review code and provide structured feedback'
|
|
90
|
+
model: claude-sonnet-4-5
|
|
91
|
+
max_turns: 20
|
|
92
|
+
timeout_minutes: 15
|
|
93
|
+
tools:
|
|
94
|
+
- Read
|
|
95
|
+
- Glob
|
|
96
|
+
- Grep
|
|
97
|
+
skills:
|
|
98
|
+
- superpowers:requesting-code-review
|
|
99
|
+
|
|
100
|
+
- name: debug
|
|
101
|
+
description: 'Diagnose and fix a failing test or runtime error'
|
|
102
|
+
model: claude-opus-4-5
|
|
103
|
+
max_turns: 30
|
|
104
|
+
timeout_minutes: 25
|
|
105
|
+
tools:
|
|
106
|
+
- Read
|
|
107
|
+
- Edit
|
|
108
|
+
- Write
|
|
109
|
+
- Bash
|
|
110
|
+
- Glob
|
|
111
|
+
- Grep
|
|
112
|
+
skills:
|
|
113
|
+
- superpowers:systematic-debugging
|
|
114
|
+
- superpowers:test-driven-development
|
|
115
|
+
|
|
116
|
+
- name: plan
|
|
117
|
+
description: 'Write an implementation plan for a feature or refactor'
|
|
118
|
+
model: claude-sonnet-4-5
|
|
119
|
+
max_turns: 15
|
|
120
|
+
timeout_minutes: 15
|
|
121
|
+
tools:
|
|
122
|
+
- Read
|
|
123
|
+
- Glob
|
|
124
|
+
- Grep
|
|
125
|
+
skills:
|
|
126
|
+
- superpowers:writing-plans
|
|
127
|
+
|
|
128
|
+
# ── Env file ──────────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
# Path to a .env file loaded before any session starts. Relative to the agent
|
|
131
|
+
# config file. Use this for secrets that differ between environments.
|
|
132
|
+
# env_file: .env
|