@myvillage/cli 1.83.0 → 1.85.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myvillage/cli",
3
- "version": "1.83.0",
3
+ "version": "1.85.0",
4
4
  "description": "MyVillageOS CLI for community developers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,103 +1,25 @@
1
1
  // ── Context Gathering ───────────────────────────────────
2
2
  // Assembles a context snapshot for each agent loop iteration.
3
3
  // This becomes the user message sent to the LLM.
4
+ //
5
+ // NOTE: This intentionally does NOT auto-fetch the community feed, network
6
+ // events, or mentions. Pushing that data into every loop of every agent was
7
+ // wasteful — most iterations don't act on it, yet each one cost three
8
+ // uncached HTTP round-trips (feed + events + mentions) plus the tokens to
9
+ // render them. Community activity is now pull-on-demand: an agent whose job
10
+ // involves the network requests it through its MCP tools (network_feed,
11
+ // network_search, network_post_get, network_comment_list, …). The instruction
12
+ // block below tells the agent how and when to go look.
4
13
 
5
- import { getAgentFeed, getAgentMentions, getAgentEvents } from '../utils/api.js';
6
14
  import { listDatasets } from '../utils/dataset-workspace.js';
7
15
  import { listModels } from '../utils/model-workspace.js';
8
16
 
9
17
  export async function gatherContext(agentConfig, lastCheckIn, recentActions = []) {
10
18
  const parts = [];
11
- let mentionsCount = 0;
12
- let eventsCount = 0;
13
19
 
14
20
  parts.push(`Current time: ${new Date().toISOString()}`);
15
21
  parts.push(`Last check-in: ${lastCheckIn || 'never'}`);
16
22
 
17
- const agentId = agentConfig.man?.agent_id;
18
-
19
- // Fetch new feed activity since last check-in. The agent-scoped feed
20
- // endpoint is enforced server-side: posts from public communities OR
21
- // communities this agent is a member of. Caller cannot widen the scope.
22
- if (agentId) {
23
- try {
24
- const params = { pageSize: 30 };
25
- if (lastCheckIn) {
26
- params.since = lastCheckIn;
27
- }
28
- const result = await getAgentFeed(agentId, params);
29
- const items = result.data || result;
30
-
31
- if (Array.isArray(items) && items.length > 0) {
32
- parts.push(`\nNew feed activity (${items.length} items):`);
33
- for (const item of items.slice(0, 20)) {
34
- const author = item.agentProfile?.handle
35
- || item.villager?.firstName
36
- || 'unknown';
37
- const community = item.community?.slug || '?';
38
- const preview = (item.body || item.title || '').slice(0, 120);
39
- parts.push(`- @${author} in r/${community}: ${preview}`);
40
- }
41
- if (items.length > 20) {
42
- parts.push(`... and ${items.length - 20} more items`);
43
- }
44
- } else {
45
- parts.push('\nNo new feed activity since last check-in.');
46
- }
47
- } catch {
48
- parts.push('\nCould not fetch feed (network error).');
49
- }
50
-
51
- // Fetch network events (NEW_POST, NEW_MEMBER, etc.) the agent has visibility for.
52
- try {
53
- const eventParams = { pageSize: 20 };
54
- if (lastCheckIn) eventParams.since = lastCheckIn;
55
- const eventResult = await getAgentEvents(agentId, eventParams);
56
- const events = eventResult.data || eventResult;
57
-
58
- if (Array.isArray(events) && events.length > 0) {
59
- eventsCount = events.length;
60
- parts.push(`\nNetwork events since last check-in (${events.length}):`);
61
- for (const e of events.slice(0, 10)) {
62
- parts.push(`- [${e.eventType}] source=${e.sourceEntityType}:${e.sourceEntityId}`);
63
- }
64
- if (events.length > 10) {
65
- parts.push(`... and ${events.length - 10} more events`);
66
- }
67
- }
68
- } catch {
69
- // Events fetch failed — not critical, skip silently
70
- }
71
-
72
- // Fetch mentions since last check-in
73
- try {
74
- const mentionParams = { pageSize: 20 };
75
- if (lastCheckIn) mentionParams.since = lastCheckIn;
76
- const mentionResult = await getAgentMentions(agentId, mentionParams);
77
- const mentions = mentionResult.data || mentionResult;
78
-
79
- if (Array.isArray(mentions) && mentions.length > 0) {
80
- mentionsCount = mentions.length;
81
- parts.push(`\nMentions since last check-in (${mentions.length}):`);
82
- for (const m of mentions.slice(0, 10)) {
83
- const author = m.agentProfile?.handle
84
- || m.villager?.firstName
85
- || 'unknown';
86
- const preview = (m.body || m.title || '').slice(0, 120);
87
- const type = m.postId ? 'comment' : 'post';
88
- parts.push(`- [${type}] @${author}: ${preview}`);
89
- }
90
- if (mentions.length > 10) {
91
- parts.push(`... and ${mentions.length - 10} more mentions`);
92
- }
93
- }
94
- } catch {
95
- // Mentions fetch failed — not critical, skip silently
96
- }
97
- } else {
98
- parts.push('\nNot registered on MAN feed yet.');
99
- }
100
-
101
23
  // Include recent actions so the agent knows what it already did
102
24
  if (recentActions.length > 0) {
103
25
  parts.push('\nYour recent actions:');
@@ -139,15 +61,19 @@ export async function gatherContext(agentConfig, lastCheckIn, recentActions = []
139
61
  /* no local models / store unavailable — skip */
140
62
  }
141
63
 
142
- parts.push('\nDecide what actions to take based on your personality and the current context.');
64
+ // Behavioral instructions.
65
+ //
66
+ // Community activity (feed / events / mentions) is deliberately NOT included
67
+ // above — it is pulled on demand instead. If engaging with the network is
68
+ // part of this agent's purpose, it must go look for itself using whatever
69
+ // community tools it has been granted.
70
+ parts.push('\nDecide what to do this iteration based on your purpose, your personality, and any active task.');
71
+ parts.push('Community activity (posts, comments, mentions) is NOT included above — nothing is pushed to you automatically. If engaging with the community is part of your job, use your tools to look: e.g. network_feed for recent activity, network_search to find specific posts or mentions of you, and network_post_get / network_comment_list to read a thread before replying. (Only relevant if those tools are available to you.)');
72
+ parts.push('Fetch only what you actually need this iteration, and only if your purpose calls for it — do not pull the feed just because you can.');
143
73
  parts.push('IMPORTANT: If you already commented on a post, only comment again if someone ELSE has replied since your last comment. Do not leave multiple unanswered comments on the same post.');
144
- parts.push('If there is no new activity and no one has responded to your previous comments, do nothing — that is a valid choice.');
145
- parts.push('You may post, comment, vote, read files, or do nothing if appropriate.');
74
+ parts.push('Doing nothing is a valid choice — if nothing needs your attention right now, take no action.');
75
+ parts.push('You may post, comment, vote, read files, run your tools, or do nothing if appropriate.');
146
76
  parts.push('Be thoughtful — quality over quantity. Avoid repeating yourself.');
147
77
 
148
- return {
149
- text: parts.join('\n'),
150
- mentionsCount,
151
- eventsCount,
152
- };
78
+ return { text: parts.join('\n') };
153
79
  }
@@ -257,11 +257,13 @@ export async function agentLoop(agentName, { signal }) {
257
257
  activeTask = await pollAndClaim(config.man.village_agent_id, agentDir);
258
258
  }
259
259
 
260
- // Gather context (returns { text, mentionsCount, eventsCount })
260
+ // Gather context (returns { text }). Community feed/events/mentions are
261
+ // no longer auto-fetched here — the agent pulls them on demand via its
262
+ // tools — so these activity counters stay at 0 unless a future signal
263
+ // repopulates them.
261
264
  const contextResult = await gatherContext(config, lastCheckIn, recentActions);
262
265
  let context = contextResult.text;
263
- mentionsFound = contextResult.mentionsCount;
264
- const eventsFound = contextResult.eventsCount || 0;
266
+ const eventsFound = 0;
265
267
 
266
268
  if (activeTask) {
267
269
  // A claimed task takes over the iteration. The default monitoring
@@ -279,16 +281,16 @@ Guidelines:
279
281
  - Use as many tool calls as needed to complete the task. A single discovery call (e.g. listing directories) is usually NOT enough — follow up with the calls that actually produce the requested result.
280
282
  - Do not stop after one tool call unless that call alone fulfills the entire instruction.
281
283
  - Do not ask clarifying questions back to the user; make a reasonable interpretation and proceed.
282
- - The feed context is provided only for situational awareness — do not let "no new feed activity" prevent you from carrying out the task.
284
+ - The ambient context below is provided only for situational awareness — carry out the task regardless of what it contains.
283
285
  - Your final text response should include the actual answer/output the user asked for (e.g. the list of files, the summary, the result), not just a description of what you did.`;
284
286
 
285
- context = `## TASK (id=${activeTask.id}, type=${activeTask.taskType})\n${instructionText}\n\n---\n\n## FEED CONTEXT (for awareness only)\n${context}`;
287
+ context = `## TASK (id=${activeTask.id}, type=${activeTask.taskType})\n${instructionText}\n\n---\n\n## AMBIENT CONTEXT (for awareness only)\n${context}`;
286
288
 
287
289
  logActivity(agentDir, { type: 'task_claimed', taskId: activeTask.id, taskType: activeTask.taskType });
288
290
  }
289
291
 
290
- // Count feed items from context
291
- feedItemsRead = (context.match(/^- @/gm) || []).length;
292
+ // Feed is no longer pushed into context; nothing to count here.
293
+ // feedItemsRead / mentionsFound stay 0 (see declarations above).
292
294
 
293
295
  logActivity(agentDir, {
294
296
  type: 'context',
@@ -46,11 +46,13 @@ import {
46
46
  getLogFilePath,
47
47
  } from '../utils/local-agent.js';
48
48
  import { scaffoldAgent, TOOL_CATALOG, resolveCatalogEntry } from '../utils/agent-scaffolder.js';
49
+ import { loadAgentRecipe, listAgentRecipes } from '../utils/agent-recipes.js';
49
50
  import { formatLocalAgentList, formatLocalAgentStatus } from '../utils/formatters.js';
51
+ import { writeFileSync } from 'fs';
50
52
 
51
53
  // ── Create Local Agent (wizard) ─────────────────────────
52
54
 
53
- export async function agentCreateLocalCommand() {
55
+ export async function agentCreateLocalCommand(options = {}) {
54
56
  if (!isAuthenticated()) {
55
57
  console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
56
58
  return;
@@ -118,7 +120,7 @@ export async function agentCreateLocalCommand() {
118
120
  {
119
121
  type: 'checkbox',
120
122
  name: 'tools',
121
- message: 'Which built-in tools should your agent have? (you can add more later with `agent add-mcp`)',
123
+ message: 'Which built-in tools should your agent have? (you can add more later with `agent add-connector`)',
122
124
  choices: [
123
125
  { name: 'MyVillageOS MCP (feed, posts, communities, wallet)', value: 'myvillage', checked: true, disabled: 'always enabled' },
124
126
  { name: 'Local Files (read/write a sandboxed workspace)', value: 'filesystem', checked: true },
@@ -223,11 +225,28 @@ export async function agentCreateLocalCommand() {
223
225
  writeAgentConfig(answers.name, cfg);
224
226
  }
225
227
 
226
- spinner.succeed('Agent scaffolded!');
228
+ // If --recipe was passed, fetch the Agent Recipe and materialize its
229
+ // prompt/tools/schedule over the freshly-scaffolded files. Best-effort:
230
+ // a fetch failure warns and leaves the blank agent in place — we never
231
+ // roll back the created agent.
232
+ let recipeApplied = null;
233
+ if (options.recipe) {
234
+ spinner.text = `Applying recipe "${options.recipe}"...`;
235
+ try {
236
+ recipeApplied = await applyAgentRecipe(answers.name, options.recipe);
237
+ } catch (err) {
238
+ recipeApplied = null;
239
+ // Downgrade spinner to a warning so the succeed() below still fires.
240
+ spinner.warn(`Recipe "${options.recipe}" not applied: ${err.message}`);
241
+ console.log(brand.teal(' Created a blank agent instead — customize it with `myvillage agent edit`.\n'));
242
+ }
243
+ }
244
+
245
+ spinner.succeed(recipeApplied ? `Agent scaffolded from recipe "${recipeApplied.name || options.recipe}"!` : 'Agent scaffolded!');
227
246
 
228
247
  console.log(brand.green(` \u2713 Agent created at ~/.myvillage/agents/${answers.name}/\n`));
229
248
  console.log(brand.teal(' Next steps:'));
230
- console.log(` ${brand.gold(`myvillage agent add-mcp ${answers.name}`)} Add a tool (GitHub, Twilio, your own MCP, etc.)`);
249
+ console.log(` ${brand.gold(`myvillage agent add-connector ${answers.name}`)} Add a tool (GitHub, Twilio, your own MCP, etc.)`);
231
250
  console.log(` ${brand.gold(`myvillage agent grant ${answers.name} <provider>`)} Authorize Google, Microsoft, or Slack`);
232
251
  console.log(` ${brand.gold(`myvillage agent start ${answers.name}`)} Launch the agent`);
233
252
  console.log(` ${brand.gold(`myvillage agent edit ${answers.name}`)} Customize the prompt`);
@@ -241,6 +260,77 @@ export async function agentCreateLocalCommand() {
241
260
  }
242
261
  }
243
262
 
263
+ // ── Agent Recipes (starter templates) ───────────────────
264
+ //
265
+ // Fetch a server-side Agent Recipe and materialize it over the three local
266
+ // files an agent already has: prompt.md (overwritten with recipe.promptMd),
267
+ // tools.yaml (myvillage.allowed_tools set to recipe.myvillageTools), and
268
+ // agent.config.yaml (schedule.check_in_interval / active_hours from
269
+ // recipe.scheduleDefaults, if present). Returns the fetched recipe on success.
270
+ // Throws on a fetch/404/network failure so the caller can warn and continue.
271
+
272
+ async function applyAgentRecipe(agentName, slug) {
273
+ const recipe = await loadAgentRecipe(slug);
274
+
275
+ // 1. prompt.md — overwrite with the recipe's prompt.
276
+ if (typeof recipe.promptMd === 'string') {
277
+ writeFileSync(join(getAgentDir(agentName), 'prompt.md'), recipe.promptMd);
278
+ }
279
+
280
+ // 2. tools.yaml — set the myvillage server's allowed_tools list.
281
+ if (Array.isArray(recipe.myvillageTools)) {
282
+ const toolsConfig = readToolsYaml(agentName);
283
+ toolsConfig.servers = toolsConfig.servers || {};
284
+ toolsConfig.servers.myvillage = toolsConfig.servers.myvillage
285
+ || resolveCatalogEntry('myvillage', agentName);
286
+ toolsConfig.servers.myvillage.allowed_tools = [...recipe.myvillageTools];
287
+ writeToolsYaml(agentName, toolsConfig);
288
+ }
289
+
290
+ // 3. agent.config.yaml — apply schedule defaults if the recipe carries any.
291
+ const sched = recipe.scheduleDefaults;
292
+ if (sched && typeof sched === 'object') {
293
+ const cfg = readAgentConfig(agentName);
294
+ cfg.schedule = cfg.schedule || {};
295
+ if (sched.check_in_interval != null) {
296
+ cfg.schedule.check_in_interval = sched.check_in_interval;
297
+ }
298
+ if (sched.active_hours && typeof sched.active_hours === 'object') {
299
+ cfg.schedule.active_hours = { ...cfg.schedule.active_hours, ...sched.active_hours };
300
+ }
301
+ writeAgentConfig(agentName, cfg);
302
+ }
303
+
304
+ return recipe;
305
+ }
306
+
307
+ export async function agentRecipesListCommand() {
308
+ if (!isAuthenticated()) {
309
+ console.log(chalk.red(" ✗ Authentication required. Run 'myvillage login' first.\n"));
310
+ return;
311
+ }
312
+
313
+ let recipes;
314
+ try {
315
+ recipes = await listAgentRecipes();
316
+ } catch (err) {
317
+ console.log(chalk.red(` ✗ ${err.message}\n`));
318
+ return;
319
+ }
320
+
321
+ if (!Array.isArray(recipes) || recipes.length === 0) {
322
+ console.log(brand.teal(' No agent recipes available yet.\n'));
323
+ return;
324
+ }
325
+
326
+ console.log(brand.teal('\n Agent recipes (create with `myvillage agent create --recipe <slug>`):\n'));
327
+ for (const r of recipes) {
328
+ console.log(` ${chalk.bold(r.slug)} ${chalk.dim(r.name || '')}`);
329
+ if (r.description) console.log(` ${chalk.dim(r.description)}`);
330
+ }
331
+ console.log('');
332
+ }
333
+
244
334
  // ── Start Agent ─────────────────────────────────────────
245
335
 
246
336
  export async function agentStartCommand(name) {
@@ -762,7 +852,7 @@ export async function agentAddToolCommand(name, tool) {
762
852
  if (!validTools.includes(tool)) {
763
853
  console.log(chalk.red(` \u2717 Unknown built-in tool "${tool}".`));
764
854
  console.log(brand.teal(` Built-in tools: ${validTools.join(', ')}`));
765
- console.log(brand.teal(` For GitHub, Twilio, custom MCPs, etc. use: ${chalk.bold(`myvillage agent add-mcp ${name}`)}`));
855
+ console.log(brand.teal(` For GitHub, Twilio, custom MCPs, etc. use: ${chalk.bold(`myvillage agent add-connector ${name}`)}`));
766
856
  console.log(brand.teal(` For Google, Microsoft, or Slack use: ${chalk.bold(`myvillage agent grant ${name} <provider>`)}\n`));
767
857
  return;
768
858
  }
@@ -8,24 +8,24 @@ import {
8
8
  isDaemonRunning,
9
9
  } from '../utils/local-agent.js';
10
10
  import {
11
- loadRecipe,
12
- listRegistryRecipes,
13
- recipeToServerEntry,
11
+ loadConnector,
12
+ listRegistryConnectors,
13
+ connectorToServerEntry,
14
14
  declaredEnvVars,
15
- } from '../utils/recipes.js';
15
+ } from '../utils/connectors.js';
16
16
  import { isAuthenticated } from '../utils/auth.js';
17
17
 
18
- // ── add-mcp ─────────────────────────────────────────────
18
+ // ── add-connector ───────────────────────────────────────
19
19
  //
20
20
  // Always interactive. If a source is provided (built-in name, local file,
21
- // or URL), the recipe's fields pre-fill the prompts but the developer can
21
+ // or URL), the connector's fields pre-fill the prompts but the developer can
22
22
  // edit every value before it's written. If no source, the wizard collects
23
23
  // the fields from scratch.
24
24
  //
25
- // Use --yes to skip prompts and accept recipe values verbatim (useful for
25
+ // Use --yes to skip prompts and accept connector values verbatim (useful for
26
26
  // scripts and CI).
27
27
 
28
- export async function agentAddMcpCommand(agentName, sourceArg, options = {}) {
28
+ export async function agentAddConnectorCommand(agentName, sourceArg, options = {}) {
29
29
  if (!agentExists(agentName)) {
30
30
  console.log(chalk.red(` ✗ Agent "${agentName}" not found. Run 'myvillage agent' to see your agents.\n`));
31
31
  return;
@@ -34,33 +34,33 @@ export async function agentAddMcpCommand(agentName, sourceArg, options = {}) {
34
34
  const source = sourceArg || options.from || null;
35
35
  const nonInteractive = options.yes === true;
36
36
 
37
- let recipe = null;
37
+ let connector = null;
38
38
  if (source) {
39
39
  try {
40
- recipe = await loadRecipe(source);
41
- console.log(brand.teal(` Loaded recipe: "${recipe.name}"${recipe.description ? ` — ${chalk.dim(recipe.description)}` : ''}\n`));
40
+ connector = await loadConnector(source);
41
+ console.log(brand.teal(` Loaded connector: "${connector.name}"${connector.description ? ` — ${chalk.dim(connector.description)}` : ''}\n`));
42
42
  } catch (err) {
43
43
  console.log(chalk.red(` ✗ ${err.message}\n`));
44
44
  return;
45
45
  }
46
46
  }
47
47
 
48
- if (!recipe && nonInteractive) {
49
- console.log(chalk.red(' ✗ --yes requires a recipe source (positional arg or --from). No source = interactive wizard.\n'));
48
+ if (!connector && nonInteractive) {
49
+ console.log(chalk.red(' ✗ --yes requires a connector source (positional arg or --from). No source = interactive wizard.\n'));
50
50
  return;
51
51
  }
52
52
 
53
- // Walk through prompts. With a recipe, each prompt is pre-filled with the
54
- // recipe's value. Without one, the developer fills in each field from
53
+ // Walk through prompts. With a connector, each prompt is pre-filled with the
54
+ // connector's value. Without one, the developer fills in each field from
55
55
  // scratch starting with the local-vs-remote choice.
56
56
  let finalized;
57
57
  try {
58
58
  finalized = nonInteractive
59
- ? recipe
60
- : await walkWizard(recipe);
59
+ ? connector
60
+ : await walkWizard(connector);
61
61
  } catch (err) {
62
62
  if (err.isTtyError) {
63
- console.log(chalk.red(' ✗ Prompts cannot be rendered in this environment. Use --yes with a recipe source for scripted setup.\n'));
63
+ console.log(chalk.red(' ✗ Prompts cannot be rendered in this environment. Use --yes with a connector source for scripted setup.\n'));
64
64
  return;
65
65
  }
66
66
  console.log(chalk.red(` ✗ ${err.message}\n`));
@@ -92,7 +92,7 @@ export async function agentAddMcpCommand(agentName, sourceArg, options = {}) {
92
92
  }
93
93
  }
94
94
 
95
- toolsConfig.servers[finalized.name] = recipeToServerEntry(finalized);
95
+ toolsConfig.servers[finalized.name] = connectorToServerEntry(finalized);
96
96
  writeToolsYaml(agentName, toolsConfig);
97
97
 
98
98
  console.log(brand.green(` ✓ Added "${finalized.name}" to agent "${agentName}".`));
@@ -120,9 +120,9 @@ export async function agentAddMcpCommand(agentName, sourceArg, options = {}) {
120
120
 
121
121
  // ── Wizard ──────────────────────────────────────────────
122
122
 
123
- async function walkWizard(recipe) {
124
- // If no recipe, ask the developer what kind of MCP they're adding
125
- if (!recipe) {
123
+ async function walkWizard(connector) {
124
+ // If no connector, ask the developer what kind of MCP they're adding
125
+ if (!connector) {
126
126
  const { kind } = await inquirer.prompt([{
127
127
  type: 'list',
128
128
  name: 'kind',
@@ -132,12 +132,12 @@ async function walkWizard(recipe) {
132
132
  { name: 'Remote URL (e.g. https://mcp.example.com)', value: 'remote' },
133
133
  ],
134
134
  }]);
135
- recipe = { name: '', description: '', env: {} };
136
- if (kind === 'remote') recipe.url = '';
137
- else recipe.command = 'npx';
135
+ connector = { name: '', description: '', env: {} };
136
+ if (kind === 'remote') connector.url = '';
137
+ else connector.command = 'npx';
138
138
  }
139
139
 
140
- const isRemote = !!recipe.url;
140
+ const isRemote = !!connector.url;
141
141
 
142
142
  // Common fields
143
143
  const baseAnswers = await inquirer.prompt([
@@ -145,7 +145,7 @@ async function walkWizard(recipe) {
145
145
  type: 'input',
146
146
  name: 'name',
147
147
  message: 'Server name (key in tools.yaml):',
148
- default: recipe.name || undefined,
148
+ default: connector.name || undefined,
149
149
  validate: v => /^[a-z0-9_-]+$/i.test((v || '').trim()) || 'lowercase letters, digits, dashes, underscores only',
150
150
  filter: v => v.trim(),
151
151
  },
@@ -153,19 +153,19 @@ async function walkWizard(recipe) {
153
153
  type: 'input',
154
154
  name: 'description',
155
155
  message: 'Description (optional, shown in tools.yaml):',
156
- default: recipe.description || '',
156
+ default: connector.description || '',
157
157
  filter: v => v.trim(),
158
158
  },
159
159
  ]);
160
160
 
161
161
  if (isRemote) {
162
- return await walkRemoteWizard(recipe, baseAnswers);
162
+ return await walkRemoteWizard(connector, baseAnswers);
163
163
  }
164
- return await walkLocalWizard(recipe, baseAnswers);
164
+ return await walkLocalWizard(connector, baseAnswers);
165
165
  }
166
166
 
167
- async function walkLocalWizard(recipe, baseAnswers) {
168
- const argsDefault = (recipe.args || []).join(' ');
167
+ async function walkLocalWizard(connector, baseAnswers) {
168
+ const argsDefault = (connector.args || []).join(' ');
169
169
  const answers = await inquirer.prompt([
170
170
  {
171
171
  type: 'input',
@@ -186,7 +186,7 @@ async function walkLocalWizard(recipe, baseAnswers) {
186
186
  type: 'input',
187
187
  name: 'envNamesRaw',
188
188
  message: 'Env vars this MCP needs (comma-separated, leave blank for none):',
189
- default: Object.keys(recipe.env || {}).join(', '),
189
+ default: Object.keys(connector.env || {}).join(', '),
190
190
  filter: v => v.trim(),
191
191
  },
192
192
  ]);
@@ -196,15 +196,15 @@ async function walkLocalWizard(recipe, baseAnswers) {
196
196
  ? answers.envNamesRaw.split(',').map(s => s.trim()).filter(Boolean)
197
197
  : [];
198
198
 
199
- // Build env schema — preserve recipe-provided descriptions for known vars,
199
+ // Build env schema — preserve connector-provided descriptions for known vars,
200
200
  // mark new ones the developer added as required-by-default with no description.
201
201
  const env = {};
202
202
  for (const name of envNames) {
203
- env[name] = recipe.env?.[name] || { description: '', required: true };
203
+ env[name] = connector.env?.[name] || { description: '', required: true };
204
204
  }
205
205
 
206
206
  return {
207
- ...recipe,
207
+ ...connector,
208
208
  ...baseAnswers,
209
209
  command: answers.command,
210
210
  args,
@@ -212,8 +212,8 @@ async function walkLocalWizard(recipe, baseAnswers) {
212
212
  };
213
213
  }
214
214
 
215
- async function walkRemoteWizard(recipe, baseAnswers) {
216
- const headerEntries = Object.entries(recipe.headers || {});
215
+ async function walkRemoteWizard(connector, baseAnswers) {
216
+ const headerEntries = Object.entries(connector.headers || {});
217
217
  const defaultAuth = headerEntries.some(([k]) => /^authorization$/i.test(k)) ? 'bearer'
218
218
  : headerEntries.length > 0 ? 'custom' : 'none';
219
219
 
@@ -247,12 +247,12 @@ async function walkRemoteWizard(recipe, baseAnswers) {
247
247
  type: 'input',
248
248
  name: 'envVarName',
249
249
  message: 'Env var name that will hold the token:',
250
- default: pickFirstEnvVarName(recipe) || 'MY_MCP_TOKEN',
250
+ default: pickFirstEnvVarName(connector) || 'MY_MCP_TOKEN',
251
251
  validate: v => /^[A-Z_][A-Z0-9_]*$/i.test(v) || 'must be a valid env-var name (letters, digits, underscores)',
252
252
  filter: v => v.trim(),
253
253
  }]);
254
254
  headers = { Authorization: `Bearer \${${envVarName}}` };
255
- env = { [envVarName]: recipe.env?.[envVarName] || { description: 'Bearer token', required: true } };
255
+ env = { [envVarName]: connector.env?.[envVarName] || { description: 'Bearer token', required: true } };
256
256
  } else if (answers.authKind === 'custom') {
257
257
  const { headersRaw } = await inquirer.prompt([{
258
258
  type: 'input',
@@ -270,23 +270,23 @@ async function walkRemoteWizard(recipe, baseAnswers) {
270
270
  for (const m of matches) envNamesFromHeaders.add(m[1]);
271
271
  }
272
272
  for (const name of envNamesFromHeaders) {
273
- env[name] = recipe.env?.[name] || { description: '', required: true };
273
+ env[name] = connector.env?.[name] || { description: '', required: true };
274
274
  }
275
275
  }
276
276
 
277
277
  return {
278
- ...recipe,
278
+ ...connector,
279
279
  ...baseAnswers,
280
280
  url: answers.url,
281
- transport: recipe.transport || 'http',
281
+ transport: connector.transport || 'http',
282
282
  headers,
283
283
  env,
284
284
  };
285
285
  }
286
286
 
287
- function pickFirstEnvVarName(recipe) {
288
- if (!recipe.env || typeof recipe.env !== 'object') return null;
289
- return Object.keys(recipe.env)[0] || null;
287
+ function pickFirstEnvVarName(connector) {
288
+ if (!connector.env || typeof connector.env !== 'object') return null;
289
+ return Object.keys(connector.env)[0] || null;
290
290
  }
291
291
 
292
292
  function parseHeaderList(raw) {
@@ -324,34 +324,34 @@ export async function agentListMcpCommand(agentName) {
324
324
  console.log('');
325
325
  }
326
326
 
327
- // ── recipes ─────────────────────────────────────────────
327
+ // ── connectors ──────────────────────────────────────────
328
328
 
329
- export async function recipesListCommand() {
329
+ export async function connectorsListCommand() {
330
330
  if (!isAuthenticated()) {
331
331
  console.log(chalk.red(" ✗ Authentication required. Run 'myvillage login' first.\n"));
332
332
  return;
333
333
  }
334
334
 
335
- let recipes;
335
+ let connectors;
336
336
  try {
337
- recipes = await listRegistryRecipes();
337
+ connectors = await listRegistryConnectors();
338
338
  } catch (err) {
339
339
  console.log(chalk.red(` ✗ ${err.message}\n`));
340
340
  return;
341
341
  }
342
342
 
343
- if (recipes.length === 0) {
344
- console.log(brand.teal(' The recipe registry is empty.\n'));
343
+ if (connectors.length === 0) {
344
+ console.log(brand.teal(' The connector registry is empty.\n'));
345
345
  return;
346
346
  }
347
347
 
348
- console.log(brand.teal('\n MyVillage curated MCP recipes (install with `agent add-mcp <agent> <name>`):\n'));
349
- for (const r of recipes) {
350
- const kind = r.kind === 'remote' ? 'remote' : 'local';
351
- console.log(` ${chalk.bold(r.name.padEnd(14))}${chalk.dim(`(${kind})`)} ${chalk.dim(r.description || '')}`);
352
- if (r.envVars && r.envVars.length > 0) {
353
- console.log(` ${' '.repeat(14)}${chalk.dim('env: ' + r.envVars.join(', '))}`);
348
+ console.log(brand.teal('\n MyVillage curated MCP connectors (install with `agent add-connector <agent> <name>`):\n'));
349
+ for (const c of connectors) {
350
+ const kind = c.kind === 'remote' ? 'remote' : 'local';
351
+ console.log(` ${chalk.bold(c.name.padEnd(14))}${chalk.dim(`(${kind})`)} ${chalk.dim(c.description || '')}`);
352
+ if (c.envVars && c.envVars.length > 0) {
353
+ console.log(` ${' '.repeat(14)}${chalk.dim('env: ' + c.envVars.join(', '))}`);
354
354
  }
355
355
  }
356
- console.log(brand.teal('\n External recipes: install with `--from <path-or-url>`.\n'));
356
+ console.log(brand.teal('\n External connectors: install with `--from <path-or-url>`.\n'));
357
357
  }
@@ -66,13 +66,19 @@ export async function agentCommand(handle) {
66
66
  }
67
67
  }
68
68
 
69
- export async function agentCreateCommand() {
69
+ export async function agentCreateCommand(options = {}) {
70
70
  if (!isAuthenticated()) {
71
71
  console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
72
72
  return;
73
73
  }
74
74
 
75
75
  try {
76
+ // A --recipe implies a local agent (recipes materialize prompt.md / tools.yaml
77
+ // / agent.config.yaml, which only local agents have) \u2014 skip the gate question.
78
+ if (options.recipe) {
79
+ return agentCreateLocalCommand(options);
80
+ }
81
+
76
82
  // Gate question: local or external?
77
83
  const { agentType } = await inquirer.prompt([{
78
84
  type: 'list',
@@ -85,7 +91,7 @@ export async function agentCreateCommand() {
85
91
  }]);
86
92
 
87
93
  if (agentType === 'local') {
88
- return agentCreateLocalCommand();
94
+ return agentCreateLocalCommand(options);
89
95
  }
90
96
 
91
97
  // Fetch the caller's existing agents to offer as parent choices for the