create-fetch-agent 0.1.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/LICENSE +21 -0
- package/README.md +151 -0
- package/bin/cli.js +102 -0
- package/package.json +55 -0
- package/src/agentverse.js +63 -0
- package/src/env.js +158 -0
- package/src/scaffold.js +316 -0
- package/src/seeds.js +12 -0
- package/src/skills.js +115 -0
- package/src/wizard.js +155 -0
- package/src/workers.js +455 -0
- package/templates/gitignore +44 -0
- package/templates/orchestrator-workers/agents/__init__.py +0 -0
- package/templates/orchestrator-workers/agents/models/__init__.py +0 -0
- package/templates/orchestrator-workers/agents/models/models.py +23 -0
- package/templates/orchestrator-workers/agents/orchestrator/__init__.py +0 -0
- package/templates/orchestrator-workers/agents/services/__init__.py +0 -0
- package/templates/orchestrator-workers/agents/services/state_service.py +22 -0
- package/templates/orchestrator-workers/requirements.txt +42 -0
- package/templates/single-agent/requirements.txt +42 -0
package/src/scaffold.js
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
workerPorts,
|
|
7
|
+
renderWorker,
|
|
8
|
+
renderConfig,
|
|
9
|
+
renderChatProtocol,
|
|
10
|
+
renderOrchestratorAgent,
|
|
11
|
+
renderMakefile,
|
|
12
|
+
renderEnv,
|
|
13
|
+
renderSingleAgent,
|
|
14
|
+
renderSingleEnv,
|
|
15
|
+
renderSingleMakefile,
|
|
16
|
+
SINGLE_AGENT_PORT,
|
|
17
|
+
ORCHESTRATOR_PORT,
|
|
18
|
+
} from "./workers.js";
|
|
19
|
+
import { renderPyproject } from "./env.js";
|
|
20
|
+
import { seed } from "./seeds.js";
|
|
21
|
+
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname = path.dirname(__filename);
|
|
24
|
+
|
|
25
|
+
export const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
26
|
+
export const TEMPLATES_DIR = path.join(PACKAGE_ROOT, "templates");
|
|
27
|
+
|
|
28
|
+
/** Build types that share the single-agent base. */
|
|
29
|
+
const SINGLE_BASE_TYPES = new Set(["single_agent", "chat_agent", "payment_agent"]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Turn a project name into a python-identifier-safe agent name.
|
|
33
|
+
*/
|
|
34
|
+
export function toAgentName(raw) {
|
|
35
|
+
const cleaned = String(raw)
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
38
|
+
.replace(/^_+|_+$/g, "")
|
|
39
|
+
.replace(/^([0-9])/, "agent_$1");
|
|
40
|
+
return cleaned || "agent";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function writeFile(targetDir, relPath, contents, written) {
|
|
44
|
+
const dest = path.join(targetDir, relPath);
|
|
45
|
+
await fs.ensureDir(path.dirname(dest));
|
|
46
|
+
await fs.writeFile(dest, contents, "utf8");
|
|
47
|
+
written.push(relPath);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function copyFile(srcAbs, targetDir, relPath, written) {
|
|
51
|
+
const dest = path.join(targetDir, relPath);
|
|
52
|
+
await fs.ensureDir(path.dirname(dest));
|
|
53
|
+
await fs.copy(srcAbs, dest);
|
|
54
|
+
written.push(relPath);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Scaffold a project from a wizard answers object.
|
|
59
|
+
*
|
|
60
|
+
* @param {object} answers
|
|
61
|
+
* @param {object} [opts]
|
|
62
|
+
* @param {string} [opts.cwd] directory the project dir is created under
|
|
63
|
+
* @param {() => string} [opts.seedFn] injectable seed generator (tests)
|
|
64
|
+
* @returns {Promise<{targetDir: string, written: string[], buildType: string}>}
|
|
65
|
+
*/
|
|
66
|
+
export async function scaffold(answers, opts = {}) {
|
|
67
|
+
const cwd = opts.cwd || process.cwd();
|
|
68
|
+
const seedFn = opts.seedFn || seed;
|
|
69
|
+
const targetDir = path.resolve(cwd, answers.projectName);
|
|
70
|
+
await fs.ensureDir(targetDir);
|
|
71
|
+
|
|
72
|
+
const written = [];
|
|
73
|
+
|
|
74
|
+
if (answers.buildType === "orchestrator_workers") {
|
|
75
|
+
await scaffoldOrchestratorWorkers(answers, { targetDir, seedFn, written });
|
|
76
|
+
} else if (SINGLE_BASE_TYPES.has(answers.buildType)) {
|
|
77
|
+
await scaffoldSingleAgent(answers, { targetDir, seedFn, written });
|
|
78
|
+
} else {
|
|
79
|
+
throw new Error(`Unknown build type: ${answers.buildType}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
written.sort();
|
|
83
|
+
return { targetDir, written, buildType: answers.buildType };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function scaffoldOrchestratorWorkers(answers, ctx) {
|
|
87
|
+
const { targetDir, seedFn, written } = ctx;
|
|
88
|
+
const names = answers.workers;
|
|
89
|
+
const ports = workerPorts(names.length);
|
|
90
|
+
const staticDir = path.join(TEMPLATES_DIR, "orchestrator-workers");
|
|
91
|
+
|
|
92
|
+
// Static, copied verbatim.
|
|
93
|
+
await copyFile(path.join(staticDir, "agents/__init__.py"), targetDir, "agents/__init__.py", written);
|
|
94
|
+
await copyFile(path.join(staticDir, "agents/models/__init__.py"), targetDir, "agents/models/__init__.py", written);
|
|
95
|
+
await copyFile(path.join(staticDir, "agents/models/models.py"), targetDir, "agents/models/models.py", written);
|
|
96
|
+
await copyFile(path.join(staticDir, "agents/services/__init__.py"), targetDir, "agents/services/__init__.py", written);
|
|
97
|
+
await copyFile(path.join(staticDir, "agents/services/state_service.py"), targetDir, "agents/services/state_service.py", written);
|
|
98
|
+
await copyFile(path.join(staticDir, "agents/orchestrator/__init__.py"), targetDir, "agents/orchestrator/__init__.py", written);
|
|
99
|
+
await copyFile(path.join(staticDir, "requirements.txt"), targetDir, "requirements.txt", written);
|
|
100
|
+
await copyFile(path.join(TEMPLATES_DIR, "gitignore"), targetDir, ".gitignore", written);
|
|
101
|
+
|
|
102
|
+
// Generated (name/count-driven).
|
|
103
|
+
await writeFile(targetDir, "agents/models/config.py", renderConfig(names), written);
|
|
104
|
+
await writeFile(targetDir, "agents/orchestrator/orchestrator_agent.py", renderOrchestratorAgent(), written);
|
|
105
|
+
await writeFile(targetDir, "agents/orchestrator/chat_protocol.py", renderChatProtocol(names), written);
|
|
106
|
+
|
|
107
|
+
for (let i = 0; i < names.length; i += 1) {
|
|
108
|
+
const name = names[i];
|
|
109
|
+
const port = ports[i];
|
|
110
|
+
await writeFile(targetDir, `agents/${name}/__init__.py`, "", written);
|
|
111
|
+
await writeFile(targetDir, `agents/${name}/${name}_agent.py`, renderWorker(name, port), written);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await writeFile(targetDir, ".env", renderEnv(names, seedFn), written);
|
|
115
|
+
await writeFile(targetDir, ".env.example", renderEnvExample(names), written);
|
|
116
|
+
await writeFile(targetDir, "Makefile", renderMakefile(names), written);
|
|
117
|
+
|
|
118
|
+
if (answers.pythonManager === "poetry") {
|
|
119
|
+
const reqs = await fs.readFile(path.join(staticDir, "requirements.txt"), "utf8");
|
|
120
|
+
await writeFile(targetDir, "pyproject.toml", renderPyproject(answers.projectName, reqs), written);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await writeFile(targetDir, "README.md", renderOrchestratorReadme(answers, names, ports), written);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function scaffoldSingleAgent(answers, ctx) {
|
|
127
|
+
const { targetDir, seedFn, written } = ctx;
|
|
128
|
+
const name = toAgentName(answers.projectName);
|
|
129
|
+
const staticDir = path.join(TEMPLATES_DIR, "single-agent");
|
|
130
|
+
|
|
131
|
+
await copyFile(path.join(staticDir, "requirements.txt"), targetDir, "requirements.txt", written);
|
|
132
|
+
await copyFile(path.join(TEMPLATES_DIR, "gitignore"), targetDir, ".gitignore", written);
|
|
133
|
+
|
|
134
|
+
await writeFile(targetDir, "agent.py", renderSingleAgent(name, SINGLE_AGENT_PORT), written);
|
|
135
|
+
await writeFile(targetDir, ".env", renderSingleEnv(seedFn), written);
|
|
136
|
+
await writeFile(targetDir, ".env.example", "AGENT_SEED_PHRASE=\n", written);
|
|
137
|
+
await writeFile(targetDir, "Makefile", renderSingleMakefile(), written);
|
|
138
|
+
|
|
139
|
+
if (answers.pythonManager === "poetry") {
|
|
140
|
+
const reqs = await fs.readFile(path.join(staticDir, "requirements.txt"), "utf8");
|
|
141
|
+
await writeFile(targetDir, "pyproject.toml", renderPyproject(answers.projectName, reqs), written);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await writeFile(targetDir, "README.md", renderSingleReadme(answers, name), written);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function renderEnvExample(names) {
|
|
148
|
+
return [
|
|
149
|
+
"# Set a unique, random seed phrase per agent (no spaces).",
|
|
150
|
+
"# `create-fetch-agent` pre-fills these in .env for you.",
|
|
151
|
+
"",
|
|
152
|
+
"ORCHESTRATOR_SEED_PHRASE=",
|
|
153
|
+
...names.map((n) => `${n.toUpperCase()}_SEED_PHRASE=`),
|
|
154
|
+
"",
|
|
155
|
+
].join("\n");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function runHints(pythonManager) {
|
|
159
|
+
if (pythonManager === "uv") {
|
|
160
|
+
return {
|
|
161
|
+
install: ["uv venv", "uv pip install -r requirements.txt"],
|
|
162
|
+
prefix: "uv run",
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (pythonManager === "poetry") {
|
|
166
|
+
return {
|
|
167
|
+
install: ["poetry install"],
|
|
168
|
+
prefix: "poetry run",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
install: ["python3.12 -m venv .venv", "source .venv/bin/activate", "pip install -r requirements.txt"],
|
|
173
|
+
prefix: "",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function renderOrchestratorReadme(answers, names, ports) {
|
|
178
|
+
const hints = runHints(answers.pythonManager);
|
|
179
|
+
const installBlock = hints.install.map((c) => c).join("\n");
|
|
180
|
+
const mk = (target) => (hints.prefix ? `${hints.prefix} make ${target}` : `make ${target}`);
|
|
181
|
+
const workerRows = names
|
|
182
|
+
.map((n, i) => `| ${n} | ${ports[i]} | \`agents/${n}/${n}_agent.py\` | \`${mk(n)}\` |`)
|
|
183
|
+
.join("\n");
|
|
184
|
+
|
|
185
|
+
return `# ${answers.projectName}
|
|
186
|
+
|
|
187
|
+
A Fetch.ai multi-agent system: an **orchestrator** (the sole ASI:One bridge) that
|
|
188
|
+
routes incoming chat messages to specialized **worker** agents. Generated with
|
|
189
|
+
[create-fetch-agent](https://github.com/anishkancherla-fetchai/create-fetch-agent).
|
|
190
|
+
|
|
191
|
+
## Architecture
|
|
192
|
+
|
|
193
|
+
\`\`\`
|
|
194
|
+
ASI:One / Agentverse
|
|
195
|
+
│ (chat protocol)
|
|
196
|
+
▼
|
|
197
|
+
orchestrator (port ${ORCHESTRATOR_PORT}) ──► routes SharedAgentState by name
|
|
198
|
+
▲ │
|
|
199
|
+
└──────── result ◄──────────────────┘
|
|
200
|
+
▼
|
|
201
|
+
${names.join(", ")}
|
|
202
|
+
\`\`\`
|
|
203
|
+
|
|
204
|
+
All agents share one message contract (\`SharedAgentState\` in
|
|
205
|
+
\`agents/models/models.py\`). The orchestrator owns the chat protocol and relays
|
|
206
|
+
the worker's \`result\` back to the user. State persists per session via
|
|
207
|
+
\`InMemoryStateService\` (swap it for Redis/Postgres without touching the pipeline).
|
|
208
|
+
Addresses are derived from seeds in \`agents/models/config.py\`, so there are no
|
|
209
|
+
hardcoded addresses.
|
|
210
|
+
|
|
211
|
+
## Agents
|
|
212
|
+
|
|
213
|
+
| Agent | Port | File | Run |
|
|
214
|
+
| ----- | ---- | ---- | --- |
|
|
215
|
+
| orchestrator | ${ORCHESTRATOR_PORT} | \`agents/orchestrator/orchestrator_agent.py\` | \`${mk("orchestrator")}\` |
|
|
216
|
+
${workerRows}
|
|
217
|
+
|
|
218
|
+
## Setup
|
|
219
|
+
|
|
220
|
+
Seeds are already generated for you in \`.env\`. Install dependencies:
|
|
221
|
+
|
|
222
|
+
\`\`\`bash
|
|
223
|
+
${installBlock}
|
|
224
|
+
\`\`\`
|
|
225
|
+
|
|
226
|
+
## Run
|
|
227
|
+
|
|
228
|
+
Each agent runs in its own terminal. Start the orchestrator first:
|
|
229
|
+
|
|
230
|
+
\`\`\`bash
|
|
231
|
+
${mk("orchestrator")}
|
|
232
|
+
\`\`\`
|
|
233
|
+
|
|
234
|
+
${names.map((n) => `\`\`\`bash\n${mk(n)}\n\`\`\``).join("\n\n")}
|
|
235
|
+
|
|
236
|
+
## Where to add your logic
|
|
237
|
+
|
|
238
|
+
Each worker has a \`<name>_workflow(state)\` function — the single extension point.
|
|
239
|
+
Read \`state.query\`, do the work, write \`state.result\`. For example, in
|
|
240
|
+
\`agents/${names[0]}/${names[0]}_agent.py\`:
|
|
241
|
+
|
|
242
|
+
\`\`\`python
|
|
243
|
+
def ${names[0]}_workflow(state: SharedAgentState) -> SharedAgentState:
|
|
244
|
+
state.result = my_llm_or_rag_call(state.query)
|
|
245
|
+
return state
|
|
246
|
+
\`\`\`
|
|
247
|
+
|
|
248
|
+
## Talk to it on ASI:One
|
|
249
|
+
|
|
250
|
+
The agents set \`mailbox=True\` and \`publish_agent_details=True\`, so you can
|
|
251
|
+
connect them through the Agentverse inspector and chat via ASI:One. See
|
|
252
|
+
"Register on Agentverse" output from the scaffolder, or the
|
|
253
|
+
[Agentverse docs](https://agentverse.ai). The inspector URL is logged on startup.
|
|
254
|
+
|
|
255
|
+
## REST hooks (custom frontend)
|
|
256
|
+
|
|
257
|
+
The orchestrator exposes \`/health\` and \`/message\` on port ${ORCHESTRATOR_PORT}:
|
|
258
|
+
|
|
259
|
+
\`\`\`bash
|
|
260
|
+
curl http://localhost:${ORCHESTRATOR_PORT}/health
|
|
261
|
+
curl -X POST http://localhost:${ORCHESTRATOR_PORT}/message -H "Content-Type: application/json" -d '{"content":"hi"}'
|
|
262
|
+
\`\`\`
|
|
263
|
+
`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function renderSingleReadme(answers, name) {
|
|
267
|
+
const hints = runHints(answers.pythonManager);
|
|
268
|
+
const installBlock = hints.install.join("\n");
|
|
269
|
+
const runCmd = hints.prefix ? `${hints.prefix} make run` : "make run";
|
|
270
|
+
const typeLabel =
|
|
271
|
+
answers.buildType === "chat_agent"
|
|
272
|
+
? "chat agent (ASI:One ready)"
|
|
273
|
+
: answers.buildType === "payment_agent"
|
|
274
|
+
? "payment agent base"
|
|
275
|
+
: "single agent";
|
|
276
|
+
|
|
277
|
+
return `# ${answers.projectName}
|
|
278
|
+
|
|
279
|
+
A Fetch.ai ${typeLabel} built on the uAgents framework. It speaks the chat
|
|
280
|
+
protocol, so it's ASI:One ready out of the box. Generated with
|
|
281
|
+
[create-fetch-agent](https://github.com/anishkancherla-fetchai/create-fetch-agent).
|
|
282
|
+
|
|
283
|
+
## Setup
|
|
284
|
+
|
|
285
|
+
The agent's seed is already generated in \`.env\`. Install dependencies:
|
|
286
|
+
|
|
287
|
+
\`\`\`bash
|
|
288
|
+
${installBlock}
|
|
289
|
+
\`\`\`
|
|
290
|
+
|
|
291
|
+
## Run
|
|
292
|
+
|
|
293
|
+
\`\`\`bash
|
|
294
|
+
${runCmd}
|
|
295
|
+
\`\`\`
|
|
296
|
+
|
|
297
|
+
The agent starts on port ${SINGLE_AGENT_PORT} and logs its address and an
|
|
298
|
+
Agentverse inspector URL.
|
|
299
|
+
|
|
300
|
+
## Where to add your logic
|
|
301
|
+
|
|
302
|
+
\`agent.py\` has an \`agent_workflow(query)\` function — the single extension point.
|
|
303
|
+
Return the response string for a given user query:
|
|
304
|
+
|
|
305
|
+
\`\`\`python
|
|
306
|
+
def agent_workflow(query: str) -> str:
|
|
307
|
+
return my_llm_call(query)
|
|
308
|
+
\`\`\`
|
|
309
|
+
|
|
310
|
+
## Talk to it on ASI:One
|
|
311
|
+
|
|
312
|
+
\`mailbox=True\` and \`publish_agent_details=True\` are set, so connect \`${name}\`
|
|
313
|
+
through the Agentverse inspector and chat with it via ASI:One. The inspector URL
|
|
314
|
+
is logged on startup.
|
|
315
|
+
`;
|
|
316
|
+
}
|
package/src/seeds.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate a unique, high-entropy seed for an agent.
|
|
5
|
+
*
|
|
6
|
+
* Each uAgent derives its on-network identity (and therefore its address) from
|
|
7
|
+
* its seed via `Identity.from_seed`. Generating these for the user means the
|
|
8
|
+
* scaffolded project runs immediately — nobody has to invent seed phrases.
|
|
9
|
+
*
|
|
10
|
+
* @returns {string} a 32-char hex string (16 random bytes)
|
|
11
|
+
*/
|
|
12
|
+
export const seed = () => randomBytes(16).toString("hex");
|
package/src/skills.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getAvailableSkills,
|
|
3
|
+
partitionSkills,
|
|
4
|
+
installSkillToCopyTarget,
|
|
5
|
+
installAgentsMd,
|
|
6
|
+
createSummary,
|
|
7
|
+
} from "fetch-skills/bin/install.js";
|
|
8
|
+
|
|
9
|
+
const PACKAGE_SKILL_BY_MANAGER = {
|
|
10
|
+
uv: "uv-package",
|
|
11
|
+
poetry: "poetry-package",
|
|
12
|
+
pip: "python-venv-package",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const COPY_TARGET_DIRS = {
|
|
16
|
+
cursor: ".cursor/skills",
|
|
17
|
+
claude: ".claude/skills",
|
|
18
|
+
antigravity: ".agent/skills",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Auto-confirm stub: a freshly scaffolded project has no pre-existing skill
|
|
22
|
+
// files, but this guarantees we never block on a prompt in non-interactive runs.
|
|
23
|
+
const autoPrompts = { confirm: async () => true };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the fetch-skills skill names implied by the wizard answers.
|
|
27
|
+
*
|
|
28
|
+
* @returns {string[]} skill names (package skill first, then protocol skills)
|
|
29
|
+
*/
|
|
30
|
+
export function selectedSkillNames(answers) {
|
|
31
|
+
const names = [];
|
|
32
|
+
const pkg = PACKAGE_SKILL_BY_MANAGER[answers.pythonManager];
|
|
33
|
+
if (pkg) names.push(pkg);
|
|
34
|
+
|
|
35
|
+
if (answers.buildType === "chat_agent" || answers.buildType === "orchestrator_workers") {
|
|
36
|
+
names.push("chat-protocol");
|
|
37
|
+
} else if (answers.buildType === "payment_agent") {
|
|
38
|
+
names.push("payment-protocol", "fet-payment-protocol", "stripe-payment-protocol");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return names;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Compute the paths fetch-skills will write for the chosen targets + skills,
|
|
46
|
+
* so the CLI's final summary shows the real locations (NOT .cursor/rules/).
|
|
47
|
+
*/
|
|
48
|
+
export function expectedSkillPaths(answers) {
|
|
49
|
+
const skillNames = selectedSkillNames(answers);
|
|
50
|
+
const paths = [];
|
|
51
|
+
for (const target of answers.aiTargets || []) {
|
|
52
|
+
if (target === "agents") {
|
|
53
|
+
paths.push("AGENTS.md");
|
|
54
|
+
} else if (COPY_TARGET_DIRS[target]) {
|
|
55
|
+
for (const name of skillNames) {
|
|
56
|
+
paths.push(`${COPY_TARGET_DIRS[target]}/${name}/SKILL.md`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return paths;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Install the selected fetch-skills context into the generated project.
|
|
65
|
+
*
|
|
66
|
+
* Calls the fetch-skills install functions directly with the pre-collected
|
|
67
|
+
* answers (no re-prompting, no shelling out to `npx fetch-skills`).
|
|
68
|
+
*
|
|
69
|
+
* @returns {Promise<{summary: object, paths: string[]}>}
|
|
70
|
+
*/
|
|
71
|
+
export async function installSkills(answers, { targetRoot, logger = console } = {}) {
|
|
72
|
+
const targets = answers.aiTargets || [];
|
|
73
|
+
const summary = createSummary();
|
|
74
|
+
|
|
75
|
+
if (targets.length === 0 || (targets.length === 1 && targets[0] === "none")) {
|
|
76
|
+
return { summary, paths: [] };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const wantedNames = new Set(selectedSkillNames(answers));
|
|
80
|
+
const available = await getAvailableSkills();
|
|
81
|
+
const skills = available.filter((s) => wantedNames.has(s.name));
|
|
82
|
+
|
|
83
|
+
// Keep partition import meaningful (and surface package vs protocol if needed).
|
|
84
|
+
partitionSkills(skills);
|
|
85
|
+
|
|
86
|
+
if (skills.length === 0) {
|
|
87
|
+
return { summary, paths: [] };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const target of targets) {
|
|
91
|
+
if (target === "none") continue;
|
|
92
|
+
if (target === "agents") {
|
|
93
|
+
await installAgentsMd({
|
|
94
|
+
skills,
|
|
95
|
+
targetRoot,
|
|
96
|
+
summary,
|
|
97
|
+
prompts: autoPrompts,
|
|
98
|
+
logger,
|
|
99
|
+
});
|
|
100
|
+
} else if (COPY_TARGET_DIRS[target]) {
|
|
101
|
+
for (const skill of skills) {
|
|
102
|
+
await installSkillToCopyTarget({
|
|
103
|
+
target,
|
|
104
|
+
skill,
|
|
105
|
+
targetRoot,
|
|
106
|
+
summary,
|
|
107
|
+
prompts: autoPrompts,
|
|
108
|
+
logger,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { summary, paths: expectedSkillPaths(answers) };
|
|
115
|
+
}
|
package/src/wizard.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { input, select, checkbox, confirm, number } from "@inquirer/prompts";
|
|
4
|
+
|
|
5
|
+
export const defaultPrompts = { input, select, checkbox, confirm, number };
|
|
6
|
+
|
|
7
|
+
const DEFAULT_WORKER_NAMES = ["alice", "bob", "carol", "dave", "erin", "frank"];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Validate a project name: no spaces, and not pointing at an existing non-empty
|
|
11
|
+
* directory. Returns true or an error string (inquirer validate convention).
|
|
12
|
+
*/
|
|
13
|
+
export function validateProjectName(name, cwd = process.cwd()) {
|
|
14
|
+
if (!name || !name.trim()) return "Project name is required.";
|
|
15
|
+
if (/\s/.test(name)) return "Project name cannot contain spaces.";
|
|
16
|
+
if (name === "." || name === "..") return "Pick a real directory name.";
|
|
17
|
+
if (/[/\\]/.test(name)) return "Project name cannot contain path separators.";
|
|
18
|
+
const target = path.resolve(cwd, name);
|
|
19
|
+
if (fs.existsSync(target)) {
|
|
20
|
+
const entries = fs.readdirSync(target);
|
|
21
|
+
if (entries.length > 0) return `Directory "${name}" already exists and is not empty.`;
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalize a worker name to a python-identifier-safe token.
|
|
28
|
+
*/
|
|
29
|
+
export function normalizeWorkerName(raw) {
|
|
30
|
+
return String(raw)
|
|
31
|
+
.toLowerCase()
|
|
32
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
33
|
+
.replace(/^_+|_+$/g, "")
|
|
34
|
+
.replace(/^([0-9])/, "w_$1");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Run the interactive wizard. All I/O is injectable so it can run headlessly
|
|
39
|
+
* in tests (pass a `prompts` object whose methods return canned answers).
|
|
40
|
+
*
|
|
41
|
+
* @returns {Promise<object>} the answers object consumed by scaffold/skills/env
|
|
42
|
+
*/
|
|
43
|
+
export async function runWizard({
|
|
44
|
+
argv = [],
|
|
45
|
+
prompts = defaultPrompts,
|
|
46
|
+
logger = console,
|
|
47
|
+
cwd = process.cwd(),
|
|
48
|
+
} = {}) {
|
|
49
|
+
let projectName = argv[0];
|
|
50
|
+
if (projectName) {
|
|
51
|
+
const valid = validateProjectName(projectName, cwd);
|
|
52
|
+
if (valid !== true) {
|
|
53
|
+
logger.log(valid);
|
|
54
|
+
projectName = undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!projectName) {
|
|
58
|
+
projectName = await prompts.input({
|
|
59
|
+
message: "Project name:",
|
|
60
|
+
default: "my-fetch-agent",
|
|
61
|
+
validate: (v) => validateProjectName(v, cwd),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const buildType = await prompts.select({
|
|
66
|
+
message: "What are you building?",
|
|
67
|
+
choices: [
|
|
68
|
+
{ name: "Single agent", value: "single_agent" },
|
|
69
|
+
{ name: "Chat agent (ASI:One ready)", value: "chat_agent" },
|
|
70
|
+
{ name: "Orchestrator + workers", value: "orchestrator_workers" },
|
|
71
|
+
{ name: "Payment agent (FET + Stripe)", value: "payment_agent" },
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
let workers = [];
|
|
76
|
+
if (buildType === "orchestrator_workers") {
|
|
77
|
+
const count = await prompts.number({
|
|
78
|
+
message: "How many worker agents?",
|
|
79
|
+
default: 2,
|
|
80
|
+
min: 1,
|
|
81
|
+
max: 10,
|
|
82
|
+
});
|
|
83
|
+
const n = Number(count) || 2;
|
|
84
|
+
const taken = new Set(["orchestrator"]);
|
|
85
|
+
for (let i = 0; i < n; i += 1) {
|
|
86
|
+
const fallback = DEFAULT_WORKER_NAMES[i] || `worker${i + 1}`;
|
|
87
|
+
// Find a default that isn't already taken.
|
|
88
|
+
let def = fallback;
|
|
89
|
+
let bump = i;
|
|
90
|
+
while (taken.has(def)) {
|
|
91
|
+
bump += 1;
|
|
92
|
+
def = DEFAULT_WORKER_NAMES[bump] || `worker${bump + 1}`;
|
|
93
|
+
}
|
|
94
|
+
const raw = await prompts.input({
|
|
95
|
+
message: `Worker ${i + 1} name:`,
|
|
96
|
+
default: def,
|
|
97
|
+
validate: (v) => {
|
|
98
|
+
const norm = normalizeWorkerName(v);
|
|
99
|
+
if (!norm) return "Enter a valid name (letters/numbers).";
|
|
100
|
+
if (norm === "orchestrator") return '"orchestrator" is reserved.';
|
|
101
|
+
if (taken.has(norm)) return `"${norm}" is already used.`;
|
|
102
|
+
return true;
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
const norm = normalizeWorkerName(raw);
|
|
106
|
+
taken.add(norm);
|
|
107
|
+
workers.push(norm);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const pythonManager = await prompts.select({
|
|
112
|
+
message: "Python setup:",
|
|
113
|
+
choices: [
|
|
114
|
+
{ name: "uv (fast, recommended)", value: "uv" },
|
|
115
|
+
{ name: "poetry", value: "poetry" },
|
|
116
|
+
{ name: "pip + venv", value: "pip" },
|
|
117
|
+
],
|
|
118
|
+
default: "uv",
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const aiTargets = await prompts.checkbox({
|
|
122
|
+
message: "Add AI-editor context? (Space to select, Enter to confirm; none = skip)",
|
|
123
|
+
choices: [
|
|
124
|
+
{ name: "Cursor", value: "cursor" },
|
|
125
|
+
{ name: "Claude Code", value: "claude" },
|
|
126
|
+
{ name: "Antigravity", value: "antigravity" },
|
|
127
|
+
{ name: "AGENTS.md", value: "agents" },
|
|
128
|
+
],
|
|
129
|
+
required: false,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const registerNow = await prompts.select({
|
|
133
|
+
message: "Register on Agentverse now?",
|
|
134
|
+
choices: [
|
|
135
|
+
{ name: "Later (just show me the steps)", value: false },
|
|
136
|
+
{ name: "Yes, show me now", value: true },
|
|
137
|
+
],
|
|
138
|
+
default: false,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const installNow = await prompts.confirm({
|
|
142
|
+
message: "Install Python dependencies now?",
|
|
143
|
+
default: true,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
projectName,
|
|
148
|
+
buildType,
|
|
149
|
+
workers,
|
|
150
|
+
pythonManager,
|
|
151
|
+
aiTargets: aiTargets || [],
|
|
152
|
+
registerNow,
|
|
153
|
+
installNow,
|
|
154
|
+
};
|
|
155
|
+
}
|