@swarp/cli 0.0.4 → 0.1.0-rc.33
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/.claude-plugin/plugin.json +7 -0
- package/.mcp.json +8 -0
- package/hooks/confirm-cost.sh +40 -0
- package/hooks/hooks.json +18 -0
- package/package.json +6 -4
- package/skills/swarp/SKILL.md +88 -0
- package/src/init/index.mjs +11 -142
- package/src/init/index.test.mjs +21 -143
- package/src/mcp-server/index.mjs +23 -11
- package/src/mcp-server/onboard.mjs +291 -0
- package/proto/swarp/v1/swarp.proto +0 -157
- package/src/init/wizard.mjs +0 -38
- package/src/skill/generate.mjs +0 -141
- package/src/templates/agent.yaml +0 -132
- package/src/templates/agent.yaml.example.hbs +0 -137
- package/src/templates/agent.yaml.hbs +0 -40
- package/src/templates/router.yaml.hbs +0 -11
- package/src/templates/workflow.yml +0 -20
- package/src/templates/workflow.yml.hbs +0 -16
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const CONFIG_PATH = resolve('.swarp.json');
|
|
6
|
+
|
|
7
|
+
const PHASES = ['prerequisites', 'router', 'secrets', 'first_agent'];
|
|
8
|
+
|
|
9
|
+
function readState() {
|
|
10
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
11
|
+
return { phases_completed: [], setup_complete: false };
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
15
|
+
} catch {
|
|
16
|
+
return { phases_completed: [], setup_complete: false };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function writeState(state) {
|
|
21
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(state, null, 2) + '\n', 'utf-8');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function completePhase(phaseName) {
|
|
25
|
+
const state = readState();
|
|
26
|
+
if (!state.phases_completed) state.phases_completed = [];
|
|
27
|
+
if (!state.phases_completed.includes(phaseName)) {
|
|
28
|
+
state.phases_completed.push(phaseName);
|
|
29
|
+
}
|
|
30
|
+
writeState(state);
|
|
31
|
+
return state;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function nextPhase() {
|
|
35
|
+
const state = readState();
|
|
36
|
+
const completed = state.phases_completed ?? [];
|
|
37
|
+
if (state.setup_complete) return null;
|
|
38
|
+
for (const phase of PHASES) {
|
|
39
|
+
if (!completed.includes(phase)) return phase;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── Phase checks ────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
function checkPrerequisites() {
|
|
47
|
+
const results = [];
|
|
48
|
+
for (const tool of ['flyctl', 'sprite', 'gh']) {
|
|
49
|
+
try {
|
|
50
|
+
execFileSync('which', [tool], { stdio: 'ignore' });
|
|
51
|
+
results.push({ tool, installed: true });
|
|
52
|
+
} catch {
|
|
53
|
+
results.push({ tool, installed: false });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return results;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function checkRouterExists(state) {
|
|
60
|
+
if (!state.fly_app) return false;
|
|
61
|
+
try {
|
|
62
|
+
const output = execFileSync('flyctl', ['apps', 'list', '--json'], {
|
|
63
|
+
encoding: 'utf-8',
|
|
64
|
+
timeout: 10000,
|
|
65
|
+
});
|
|
66
|
+
const apps = JSON.parse(output);
|
|
67
|
+
return apps.some((a) => a.Name === state.fly_app || a.name === state.fly_app);
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Tool handler ────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
export async function handleOnboard(toolArgs) {
|
|
76
|
+
const { action } = toolArgs ?? {};
|
|
77
|
+
const state = readState();
|
|
78
|
+
const phase = nextPhase();
|
|
79
|
+
|
|
80
|
+
// Status check — no action, just report
|
|
81
|
+
if (!action || action === 'status') {
|
|
82
|
+
if (state.setup_complete) {
|
|
83
|
+
return {
|
|
84
|
+
content: [{
|
|
85
|
+
type: 'text',
|
|
86
|
+
text: [
|
|
87
|
+
'SWARP setup is complete.',
|
|
88
|
+
`Router: ${state.router_url ?? 'not set'}`,
|
|
89
|
+
`Fly app: ${state.fly_app ?? 'not set'}`,
|
|
90
|
+
`Agents dir: ${state.agents_dir ?? 'agents/'}`,
|
|
91
|
+
`Phases completed: ${(state.phases_completed ?? []).join(', ')}`,
|
|
92
|
+
'',
|
|
93
|
+
'Use swarp_status to check agent health, or swarp_deploy to deploy agents.',
|
|
94
|
+
].join('\n'),
|
|
95
|
+
}],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!phase) {
|
|
100
|
+
return { content: [{ type: 'text', text: 'All phases complete. Run with action="finish" to finalize setup.' }] };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
content: [{
|
|
105
|
+
type: 'text',
|
|
106
|
+
text: [
|
|
107
|
+
`SWARP onboarding — next phase: ${phase}`,
|
|
108
|
+
`Completed: ${(state.phases_completed ?? []).join(', ') || 'none'}`,
|
|
109
|
+
'',
|
|
110
|
+
getPhaseInstructions(phase, state),
|
|
111
|
+
].join('\n'),
|
|
112
|
+
}],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Complete a phase
|
|
117
|
+
if (action === 'complete_phase') {
|
|
118
|
+
const { phase: phaseName } = toolArgs;
|
|
119
|
+
if (!phaseName || !PHASES.includes(phaseName)) {
|
|
120
|
+
return { content: [{ type: 'text', text: `Invalid phase: ${phaseName}. Valid: ${PHASES.join(', ')}` }], isError: true };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Validate phase ordering
|
|
124
|
+
const completed = state.phases_completed ?? [];
|
|
125
|
+
const phaseIdx = PHASES.indexOf(phaseName);
|
|
126
|
+
for (let i = 0; i < phaseIdx; i++) {
|
|
127
|
+
if (!completed.includes(PHASES[i])) {
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: 'text', text: `Cannot complete "${phaseName}" — phase "${PHASES[i]}" must be completed first.` }],
|
|
130
|
+
isError: true,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const updated = completePhase(phaseName);
|
|
136
|
+
const next = nextPhase();
|
|
137
|
+
|
|
138
|
+
if (!next) {
|
|
139
|
+
return {
|
|
140
|
+
content: [{ type: 'text', text: `Phase "${phaseName}" complete. All phases done! Run with action="finish" to finalize.` }],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
content: [{
|
|
146
|
+
type: 'text',
|
|
147
|
+
text: [
|
|
148
|
+
`Phase "${phaseName}" complete.`,
|
|
149
|
+
'',
|
|
150
|
+
`Next phase: ${next}`,
|
|
151
|
+
getPhaseInstructions(next, updated),
|
|
152
|
+
].join('\n'),
|
|
153
|
+
}],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Save config values
|
|
158
|
+
if (action === 'save_config') {
|
|
159
|
+
const { key, value } = toolArgs;
|
|
160
|
+
if (!key || value === undefined) {
|
|
161
|
+
return { content: [{ type: 'text', text: 'save_config requires key and value' }], isError: true };
|
|
162
|
+
}
|
|
163
|
+
const updated = readState();
|
|
164
|
+
updated[key] = value;
|
|
165
|
+
writeState(updated);
|
|
166
|
+
return { content: [{ type: 'text', text: `Saved ${key}=${JSON.stringify(value)} to .swarp.json` }] };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Finalize setup
|
|
170
|
+
if (action === 'finish') {
|
|
171
|
+
const completed = state.phases_completed ?? [];
|
|
172
|
+
const missing = PHASES.filter((p) => !completed.includes(p));
|
|
173
|
+
if (missing.length > 0) {
|
|
174
|
+
return {
|
|
175
|
+
content: [{ type: 'text', text: `Cannot finalize — incomplete phases: ${missing.join(', ')}` }],
|
|
176
|
+
isError: true,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const updated = readState();
|
|
180
|
+
updated.setup_complete = true;
|
|
181
|
+
writeState(updated);
|
|
182
|
+
return { content: [{ type: 'text', text: 'SWARP setup complete! Use swarp_status to check your agents.' }] };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check prerequisites
|
|
186
|
+
if (action === 'check_prerequisites') {
|
|
187
|
+
const results = checkPrerequisites();
|
|
188
|
+
const allInstalled = results.every((r) => r.installed);
|
|
189
|
+
const lines = results.map((r) => ` ${r.installed ? '✓' : '✗'} ${r.tool}`);
|
|
190
|
+
return {
|
|
191
|
+
content: [{
|
|
192
|
+
type: 'text',
|
|
193
|
+
text: [
|
|
194
|
+
'Prerequisite check:',
|
|
195
|
+
...lines,
|
|
196
|
+
'',
|
|
197
|
+
allInstalled
|
|
198
|
+
? 'All prerequisites met. Run swarp_onboard with action="complete_phase" phase="prerequisites" to advance.'
|
|
199
|
+
: 'Missing tools must be installed before continuing.',
|
|
200
|
+
].join('\n'),
|
|
201
|
+
}],
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { content: [{ type: 'text', text: `Unknown action: ${action}` }], isError: true };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function getPhaseInstructions(phase, state) {
|
|
209
|
+
switch (phase) {
|
|
210
|
+
case 'prerequisites':
|
|
211
|
+
return [
|
|
212
|
+
'Check that required CLIs are installed:',
|
|
213
|
+
' - flyctl (Fly.io CLI): https://fly.io/docs/getting-started/installing-flyctl/',
|
|
214
|
+
' - sprite (Fly Sprites CLI): https://sprites.dev',
|
|
215
|
+
' - gh (GitHub CLI): https://cli.github.com',
|
|
216
|
+
'',
|
|
217
|
+
'Run swarp_onboard with action="check_prerequisites" to verify.',
|
|
218
|
+
].join('\n');
|
|
219
|
+
|
|
220
|
+
case 'router':
|
|
221
|
+
return [
|
|
222
|
+
'Deploy the SWARP router to Fly.io.',
|
|
223
|
+
'',
|
|
224
|
+
'Steps:',
|
|
225
|
+
'1. Ask the user for their Fly.io org name',
|
|
226
|
+
'2. Use swarp_deploy_router tool with the org name',
|
|
227
|
+
' (This will show a cost estimate and require user approval)',
|
|
228
|
+
'3. Save router_url and fly_app to config:',
|
|
229
|
+
' swarp_onboard action="save_config" key="router_url" value="<url>"',
|
|
230
|
+
' swarp_onboard action="save_config" key="fly_app" value="<app-name>"',
|
|
231
|
+
' swarp_onboard action="save_config" key="fly_org" value="<org>"',
|
|
232
|
+
'4. Complete: swarp_onboard action="complete_phase" phase="router"',
|
|
233
|
+
].join('\n');
|
|
234
|
+
|
|
235
|
+
case 'secrets':
|
|
236
|
+
return [
|
|
237
|
+
'Set up GitHub repo secrets for CI deployment.',
|
|
238
|
+
'',
|
|
239
|
+
'Tell the user to set these in their repo (Settings → Secrets → Actions):',
|
|
240
|
+
` SWARP_SPRITES_TOKEN — from https://sprites.dev/account/${state.fly_org ?? '{org}'}/tokens`,
|
|
241
|
+
' SWARP_FLY_API_TOKEN — run: flyctl tokens create deploy',
|
|
242
|
+
'',
|
|
243
|
+
'Verify local access:',
|
|
244
|
+
` flyctl status --app ${state.fly_app ?? 'swarp-router'}`,
|
|
245
|
+
' sprite list',
|
|
246
|
+
'',
|
|
247
|
+
'Note: This verifies LOCAL credentials only. CI secrets must be confirmed separately.',
|
|
248
|
+
'Optionally check: gh secret list',
|
|
249
|
+
'',
|
|
250
|
+
'Complete: swarp_onboard action="complete_phase" phase="secrets"',
|
|
251
|
+
].join('\n');
|
|
252
|
+
|
|
253
|
+
case 'first_agent':
|
|
254
|
+
return [
|
|
255
|
+
'Create and deploy the first agent.',
|
|
256
|
+
'',
|
|
257
|
+
'Steps:',
|
|
258
|
+
'1. Ask the user what the agent should do',
|
|
259
|
+
'2. Generate agents/<name>/agent.yaml with appropriate modes',
|
|
260
|
+
'3. Use swarp_deploy tool to deploy the agent',
|
|
261
|
+
' (This will show a cost estimate and require user approval)',
|
|
262
|
+
'4. Verify with swarp_status',
|
|
263
|
+
'5. Complete: swarp_onboard action="complete_phase" phase="first_agent"',
|
|
264
|
+
].join('\n');
|
|
265
|
+
|
|
266
|
+
default:
|
|
267
|
+
return `Unknown phase: ${phase}`;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export const onboardToolDef = {
|
|
272
|
+
name: 'swarp_onboard',
|
|
273
|
+
description: 'Manage SWARP onboarding workflow. Tracks setup progress, enforces phase ordering, saves configuration.',
|
|
274
|
+
inputSchema: {
|
|
275
|
+
type: 'object',
|
|
276
|
+
properties: {
|
|
277
|
+
action: {
|
|
278
|
+
type: 'string',
|
|
279
|
+
enum: ['status', 'check_prerequisites', 'complete_phase', 'save_config', 'finish'],
|
|
280
|
+
description: 'Action to perform. Default: status (show current phase).',
|
|
281
|
+
},
|
|
282
|
+
phase: {
|
|
283
|
+
type: 'string',
|
|
284
|
+
enum: ['prerequisites', 'router', 'secrets', 'first_agent'],
|
|
285
|
+
description: 'Phase name (for complete_phase action)',
|
|
286
|
+
},
|
|
287
|
+
key: { type: 'string', description: 'Config key (for save_config action)' },
|
|
288
|
+
value: { type: 'string', description: 'Config value (for save_config action)' },
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
};
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
syntax = "proto3";
|
|
2
|
-
package swarp.v1;
|
|
3
|
-
|
|
4
|
-
option go_package = "github.com/dl3consulting/swarp/gen/go/swarp/v1;swarpv1";
|
|
5
|
-
|
|
6
|
-
// Router <-> Sprite communication
|
|
7
|
-
service AgentRunnerService {
|
|
8
|
-
rpc Dispatch(DispatchRequest) returns (stream DispatchResponse);
|
|
9
|
-
rpc GetStatus(GetStatusRequest) returns (GetStatusResponse);
|
|
10
|
-
rpc GetHealth(GetHealthRequest) returns (GetHealthResponse);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Sprite -> Router registration
|
|
14
|
-
service AgentRegistryService {
|
|
15
|
-
rpc Register(RegisterRequest) returns (RegisterResponse);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Plugin/MCP -> Router communication
|
|
19
|
-
// Plugins must implement DispatchTask. ListAgents, CancelTask, GetAgentStatus are optional.
|
|
20
|
-
service AgentDispatchService {
|
|
21
|
-
rpc DispatchTask(DispatchTaskRequest) returns (stream DispatchTaskResponse);
|
|
22
|
-
rpc ListAgents(ListAgentsRequest) returns (ListAgentsResponse);
|
|
23
|
-
rpc CancelTask(CancelTaskRequest) returns (CancelTaskResponse);
|
|
24
|
-
rpc GetAgentStatus(GetAgentStatusRequest) returns (GetAgentStatusResponse);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
message RegisterRequest {
|
|
28
|
-
string name = 1;
|
|
29
|
-
string address = 2;
|
|
30
|
-
repeated ModeInfo modes = 3;
|
|
31
|
-
string version = 4;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
message RegisterResponse {
|
|
35
|
-
bool accepted = 1;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
message DispatchRequest {
|
|
39
|
-
string mode = 1;
|
|
40
|
-
map<string, string> params = 2;
|
|
41
|
-
string session_id = 3;
|
|
42
|
-
int32 remaining_budget = 4;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
message DispatchTaskRequest {
|
|
46
|
-
string agent = 1;
|
|
47
|
-
|
|
48
|
-
oneof dispatch {
|
|
49
|
-
StructuredDispatch structured = 2;
|
|
50
|
-
string raw_text = 3;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
string callback_id = 4;
|
|
54
|
-
string session_id = 5;
|
|
55
|
-
int32 remaining_budget = 6;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
message DispatchResponse {
|
|
59
|
-
AgentEvent event = 1;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
message DispatchTaskResponse {
|
|
63
|
-
AgentEvent event = 1;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
message StructuredDispatch {
|
|
67
|
-
string mode = 1;
|
|
68
|
-
map<string, string> params = 2;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
message AgentEvent {
|
|
72
|
-
enum EventType {
|
|
73
|
-
EVENT_TYPE_UNSPECIFIED = 0;
|
|
74
|
-
EVENT_TYPE_THINKING = 1;
|
|
75
|
-
EVENT_TYPE_TOOL_USE = 2;
|
|
76
|
-
EVENT_TYPE_DONE = 3;
|
|
77
|
-
EVENT_TYPE_ERROR = 4;
|
|
78
|
-
}
|
|
79
|
-
EventType type = 1;
|
|
80
|
-
string text = 2;
|
|
81
|
-
string task_id = 3;
|
|
82
|
-
string session_id = 4;
|
|
83
|
-
TaskResult result = 5;
|
|
84
|
-
int64 timestamp = 6;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
message TaskResult {
|
|
88
|
-
string status = 1;
|
|
89
|
-
string summary = 2;
|
|
90
|
-
map<string, string> structured = 3;
|
|
91
|
-
CostInfo cost = 4;
|
|
92
|
-
repeated string notify = 5;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
message CostInfo {
|
|
96
|
-
double usd = 1;
|
|
97
|
-
int32 turns = 2;
|
|
98
|
-
int64 duration_ms = 3;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
message GetStatusRequest {}
|
|
102
|
-
|
|
103
|
-
message GetStatusResponse {
|
|
104
|
-
string status = 1;
|
|
105
|
-
string task_id = 2;
|
|
106
|
-
string mode = 3;
|
|
107
|
-
string session_id = 4;
|
|
108
|
-
TaskResult result = 5;
|
|
109
|
-
string last_output = 6;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
message GetHealthRequest {}
|
|
113
|
-
|
|
114
|
-
message GetHealthResponse {
|
|
115
|
-
bool up = 1;
|
|
116
|
-
bool has_task = 2;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
message CancelTaskRequest {
|
|
120
|
-
string task_id = 1;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
message CancelTaskResponse {
|
|
124
|
-
string status = 1;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
message ListAgentsRequest {}
|
|
128
|
-
|
|
129
|
-
message ListAgentsResponse {
|
|
130
|
-
repeated AgentInfo agents = 1;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
message AgentInfo {
|
|
134
|
-
string name = 1;
|
|
135
|
-
string display_name = 2;
|
|
136
|
-
string role = 3;
|
|
137
|
-
repeated ModeInfo modes = 4;
|
|
138
|
-
bool online = 5;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
message ModeInfo {
|
|
142
|
-
string name = 1;
|
|
143
|
-
string description = 2;
|
|
144
|
-
string model = 3;
|
|
145
|
-
int32 max_turns = 4;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
message GetAgentStatusRequest {
|
|
149
|
-
string agent = 1;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
message GetAgentStatusResponse {
|
|
153
|
-
bool up = 1;
|
|
154
|
-
string status = 2;
|
|
155
|
-
string mode = 3;
|
|
156
|
-
CostInfo last_cost = 4;
|
|
157
|
-
}
|
package/src/init/wizard.mjs
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import readline from 'node:readline';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Prompts the user for a value, showing a default in brackets.
|
|
5
|
-
* Returns the default if the user presses Enter without typing.
|
|
6
|
-
*/
|
|
7
|
-
function prompt(rl, question, defaultValue) {
|
|
8
|
-
return new Promise((resolve) => {
|
|
9
|
-
const display = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
|
|
10
|
-
rl.question(display, (answer) => {
|
|
11
|
-
resolve(answer.trim() || defaultValue || '');
|
|
12
|
-
});
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Runs the interactive CLI wizard.
|
|
18
|
-
* Returns an object with the user's answers.
|
|
19
|
-
*
|
|
20
|
-
* @param {object} [opts] - Optional overrides for testing
|
|
21
|
-
* @param {NodeJS.ReadableStream} [opts.input] - Readable stream (default: process.stdin)
|
|
22
|
-
* @param {NodeJS.WritableStream} [opts.output] - Writable stream (default: process.stdout)
|
|
23
|
-
* @returns {Promise<{agentsDir: string, flyOrg: string, firstAgentName: string}>}
|
|
24
|
-
*/
|
|
25
|
-
export async function runWizard({ input = process.stdin, output = process.stdout } = {}) {
|
|
26
|
-
const rl = readline.createInterface({ input, output, terminal: false });
|
|
27
|
-
|
|
28
|
-
output.write('\nSWARP init — setting up your agent swarm\n');
|
|
29
|
-
output.write('─'.repeat(44) + '\n\n');
|
|
30
|
-
|
|
31
|
-
const agentsDir = await prompt(rl, 'Agents directory', 'agents/');
|
|
32
|
-
const flyOrg = await prompt(rl, 'Fly.io org name', '');
|
|
33
|
-
const firstAgentName = await prompt(rl, 'First agent name', 'example');
|
|
34
|
-
|
|
35
|
-
rl.close();
|
|
36
|
-
|
|
37
|
-
return { agentsDir, flyOrg, firstAgentName };
|
|
38
|
-
}
|
package/src/skill/generate.mjs
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
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
|
-
}
|