@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 +1 -1
- package/src/agent-runtime/context.js +21 -95
- package/src/agent-runtime/loop.js +9 -7
- package/src/commands/agent-local.js +95 -5
- package/src/commands/agent-mcp.js +59 -59
- package/src/commands/agent.js +8 -2
- package/src/index.js +24 -13
- package/src/utils/agent-recipes.js +83 -0
- package/src/utils/agent-scaffolder.js +6 -6
- package/src/utils/connectors.js +285 -0
- package/src/utils/recipes.js +18 -275
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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('
|
|
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
|
|
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
|
-
|
|
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
|
|
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##
|
|
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
|
-
//
|
|
291
|
-
feedItemsRead
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
loadConnector,
|
|
12
|
+
listRegistryConnectors,
|
|
13
|
+
connectorToServerEntry,
|
|
14
14
|
declaredEnvVars,
|
|
15
|
-
} from '../utils/
|
|
15
|
+
} from '../utils/connectors.js';
|
|
16
16
|
import { isAuthenticated } from '../utils/auth.js';
|
|
17
17
|
|
|
18
|
-
// ── add-
|
|
18
|
+
// ── add-connector ───────────────────────────────────────
|
|
19
19
|
//
|
|
20
20
|
// Always interactive. If a source is provided (built-in name, local file,
|
|
21
|
-
// or URL), the
|
|
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
|
|
25
|
+
// Use --yes to skip prompts and accept connector values verbatim (useful for
|
|
26
26
|
// scripts and CI).
|
|
27
27
|
|
|
28
|
-
export async function
|
|
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
|
|
37
|
+
let connector = null;
|
|
38
38
|
if (source) {
|
|
39
39
|
try {
|
|
40
|
-
|
|
41
|
-
console.log(brand.teal(` Loaded
|
|
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 (!
|
|
49
|
-
console.log(chalk.red(' ✗ --yes requires a
|
|
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
|
|
54
|
-
//
|
|
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
|
-
?
|
|
60
|
-
: await walkWizard(
|
|
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
|
|
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] =
|
|
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(
|
|
124
|
-
// If no
|
|
125
|
-
if (!
|
|
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
|
-
|
|
136
|
-
if (kind === 'remote')
|
|
137
|
-
else
|
|
135
|
+
connector = { name: '', description: '', env: {} };
|
|
136
|
+
if (kind === 'remote') connector.url = '';
|
|
137
|
+
else connector.command = 'npx';
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
const isRemote = !!
|
|
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:
|
|
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:
|
|
156
|
+
default: connector.description || '',
|
|
157
157
|
filter: v => v.trim(),
|
|
158
158
|
},
|
|
159
159
|
]);
|
|
160
160
|
|
|
161
161
|
if (isRemote) {
|
|
162
|
-
return await walkRemoteWizard(
|
|
162
|
+
return await walkRemoteWizard(connector, baseAnswers);
|
|
163
163
|
}
|
|
164
|
-
return await walkLocalWizard(
|
|
164
|
+
return await walkLocalWizard(connector, baseAnswers);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
async function walkLocalWizard(
|
|
168
|
-
const argsDefault = (
|
|
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(
|
|
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
|
|
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] =
|
|
203
|
+
env[name] = connector.env?.[name] || { description: '', required: true };
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
return {
|
|
207
|
-
...
|
|
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(
|
|
216
|
-
const headerEntries = Object.entries(
|
|
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(
|
|
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]:
|
|
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] =
|
|
273
|
+
env[name] = connector.env?.[name] || { description: '', required: true };
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
return {
|
|
278
|
-
...
|
|
278
|
+
...connector,
|
|
279
279
|
...baseAnswers,
|
|
280
280
|
url: answers.url,
|
|
281
|
-
transport:
|
|
281
|
+
transport: connector.transport || 'http',
|
|
282
282
|
headers,
|
|
283
283
|
env,
|
|
284
284
|
};
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
function pickFirstEnvVarName(
|
|
288
|
-
if (!
|
|
289
|
-
return Object.keys(
|
|
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
|
-
// ──
|
|
327
|
+
// ── connectors ──────────────────────────────────────────
|
|
328
328
|
|
|
329
|
-
export async function
|
|
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
|
|
335
|
+
let connectors;
|
|
336
336
|
try {
|
|
337
|
-
|
|
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 (
|
|
344
|
-
console.log(brand.teal(' The
|
|
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
|
|
349
|
-
for (const
|
|
350
|
-
const kind =
|
|
351
|
-
console.log(` ${chalk.bold(
|
|
352
|
-
if (
|
|
353
|
-
console.log(` ${' '.repeat(14)}${chalk.dim('env: ' +
|
|
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
|
|
356
|
+
console.log(brand.teal('\n External connectors: install with `--from <path-or-url>`.\n'));
|
|
357
357
|
}
|
package/src/commands/agent.js
CHANGED
|
@@ -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
|