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.
@@ -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
+ }