@myvillage/cli 1.26.0 → 1.30.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.26.0",
3
+ "version": "1.30.0",
4
4
  "description": "MyVillageOS CLI for community developers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,7 +2,7 @@
2
2
  // Core agent loop using Vercel AI SDK. Reads prompt.md,
3
3
  // gathers context, calls LLM with tools, logs results.
4
4
 
5
- import { generateText } from 'ai';
5
+ import { generateText, stepCountIs } from 'ai';
6
6
  import { createAnthropic } from '@ai-sdk/anthropic';
7
7
  import { createOpenAI } from '@ai-sdk/openai';
8
8
  import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from 'fs';
@@ -176,7 +176,15 @@ export async function agentLoop(agentName, { signal }) {
176
176
  const instructionText = activeTask.instruction
177
177
  || (activeTask.input ? JSON.stringify(activeTask.input, null, 2) : '');
178
178
 
179
- systemPrompt = `${systemPrompt}\n\n## ACTIVE TASK MODE\nA client has assigned you a task. Your job this iteration is to execute the instruction below using your available tools. The feed context is provided only for situational awareness — do not let "no new feed activity" prevent you from carrying out the task.`;
179
+ systemPrompt = `${systemPrompt}\n\n## ACTIVE TASK MODE
180
+ A client has assigned you the task below. Your job this iteration is to **execute it fully** using your available tools, not just acknowledge it.
181
+
182
+ Guidelines:
183
+ - 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.
184
+ - Do not stop after one tool call unless that call alone fulfills the entire instruction.
185
+ - Do not ask clarifying questions back to the user; make a reasonable interpretation and proceed.
186
+ - The feed context is provided only for situational awareness — do not let "no new feed activity" prevent you from carrying out the task.
187
+ - 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.`;
180
188
 
181
189
  context = `## TASK (id=${activeTask.id}, type=${activeTask.taskType})\n${instructionText}\n\n---\n\n## FEED CONTEXT (for awareness only)\n${context}`;
182
190
 
@@ -193,13 +201,20 @@ export async function agentLoop(agentName, { signal }) {
193
201
  });
194
202
 
195
203
  // Call LLM with tools
204
+ // AI SDK v6 removed `maxSteps` — the loop now uses `stopWhen` with
205
+ // a StopCondition predicate. The default is `stepCountIs(1)`, so
206
+ // without this the model gets exactly ONE round of tool calls and
207
+ // returns immediately. We give it up to 10 steps so a task like
208
+ // "find any README under the workspace" can do
209
+ // list_allowed_directories → list_directory → search_files → ...
210
+ // until it's actually done.
196
211
  const result = await generateText({
197
212
  model,
198
213
  system: systemPrompt,
199
214
  prompt: context,
200
215
  tools,
201
- maxSteps: 5,
202
- maxTokens,
216
+ stopWhen: stepCountIs(10),
217
+ maxOutputTokens: maxTokens,
203
218
  });
204
219
 
205
220
  // Log LLM response
@@ -215,6 +230,10 @@ export async function agentLoop(agentName, { signal }) {
215
230
 
216
231
  // Log tool calls and count activity. Also audit action-tool success
217
232
  // so we don't trust the model's final text about whether a task worked.
233
+ // `toolCallSummary` accumulates a structured per-call record that we
234
+ // attach to the task output so the Activity panel can show what the
235
+ // agent actually did — not just a tool-call count.
236
+ const toolCallSummary = [];
218
237
  if (result.steps?.length) {
219
238
  for (const step of result.steps) {
220
239
  if (step.toolCalls?.length) {
@@ -228,25 +247,39 @@ export async function agentLoop(agentName, { signal }) {
228
247
  if (step.toolCalls?.length && step.toolResults?.length) {
229
248
  for (let i = 0; i < step.toolResults.length; i++) {
230
249
  const tr = step.toolResults[i];
231
- const args = step.toolCalls[i]?.args;
250
+ // AI SDK v6 renamed `args` `input`. Try v6 first.
251
+ const args = step.toolCalls[i]?.input ?? step.toolCalls[i]?.args;
232
252
  const errored = isToolResultError(tr);
253
+ const resultSummary = summarizeToolResult(tr);
233
254
  auditToolCall(taskActionAudit, tr.toolName, errored, tr);
255
+ toolCallSummary.push({
256
+ tool: tr.toolName,
257
+ args,
258
+ result: resultSummary,
259
+ ok: !errored,
260
+ });
234
261
  logActivity(agentDir, {
235
262
  type: 'tool_call',
236
263
  tool: tr.toolName,
237
264
  args,
238
- result: summarizeToolResult(tr),
265
+ result: resultSummary,
239
266
  ok: !errored,
240
267
  });
241
268
  }
242
269
  } else if (step.toolResults?.length) {
243
270
  for (const tr of step.toolResults) {
244
271
  const errored = isToolResultError(tr);
272
+ const resultSummary = summarizeToolResult(tr);
245
273
  auditToolCall(taskActionAudit, tr.toolName, errored, tr);
274
+ toolCallSummary.push({
275
+ tool: tr.toolName,
276
+ result: resultSummary,
277
+ ok: !errored,
278
+ });
246
279
  logActivity(agentDir, {
247
280
  type: 'tool_call',
248
281
  tool: tr.toolName,
249
- result: summarizeToolResult(tr),
282
+ result: resultSummary,
250
283
  ok: !errored,
251
284
  });
252
285
  }
@@ -259,10 +292,16 @@ export async function agentLoop(agentName, { signal }) {
259
292
  if (tc.toolName === 'comment_create') activity.commentsCreated++;
260
293
  if (tc.toolName === 'vote_cast') activity.votesGiven++;
261
294
  // No paired result here — assume executed, can't audit.
295
+ const args = tc.input ?? tc.args;
296
+ toolCallSummary.push({
297
+ tool: tc.toolName,
298
+ args,
299
+ result: 'executed',
300
+ });
262
301
  logActivity(agentDir, {
263
302
  type: 'tool_call',
264
303
  tool: tc.toolName,
265
- args: tc.args,
304
+ args,
266
305
  result: 'executed',
267
306
  });
268
307
  }
@@ -281,12 +320,12 @@ export async function agentLoop(agentName, { signal }) {
281
320
  if (result.steps?.length) {
282
321
  for (const step of result.steps) {
283
322
  for (const tc of step.toolCalls || []) {
284
- collectActions(tc.toolName, tc.args);
323
+ collectActions(tc.toolName, tc.input ?? tc.args);
285
324
  }
286
325
  }
287
326
  } else if (result.toolCalls?.length) {
288
327
  for (const tc of result.toolCalls) {
289
- collectActions(tc.toolName, tc.args);
328
+ collectActions(tc.toolName, tc.input ?? tc.args);
290
329
  }
291
330
  }
292
331
  // Keep only last 50 actions to bound memory
@@ -310,7 +349,8 @@ export async function agentLoop(agentName, { signal }) {
310
349
  errorMessage,
311
350
  output: {
312
351
  text: result.text || '',
313
- toolCalls: activity.toolCalls,
352
+ toolCallCount: activity.toolCalls,
353
+ toolCalls: toolCallSummary,
314
354
  toolErrors: taskActionAudit.toolErrors,
315
355
  note: 'Marked FAILED because the action tools did not succeed. The model\'s text may claim success but the underlying tool calls errored.',
316
356
  },
@@ -326,7 +366,8 @@ export async function agentLoop(agentName, { signal }) {
326
366
  await completeAgentTask(config.man.village_agent_id, activeTask.id, {
327
367
  output: {
328
368
  text: result.text || '',
329
- toolCalls: activity.toolCalls,
369
+ toolCallCount: activity.toolCalls,
370
+ toolCalls: toolCallSummary,
330
371
  toolErrors: taskActionAudit.toolErrors.length > 0 ? taskActionAudit.toolErrors : undefined,
331
372
  },
332
373
  tokensUsed: result.usage?.totalTokens || 0,
@@ -452,7 +493,10 @@ const ACTION_TOOLS = new Set([
452
493
 
453
494
  function flattenToolResultText(tr) {
454
495
  if (!tr) return '';
455
- const r = tr.result;
496
+ // AI SDK v6 renamed `result` → `output`. Fall back to the old name so
497
+ // older SDK versions still work if anyone pins them.
498
+ const r = tr.output ?? tr.result;
499
+ if (r == null) return '';
456
500
  if (typeof r === 'string') return r;
457
501
  if (Array.isArray(r?.content)) {
458
502
  return r.content
@@ -102,15 +102,23 @@ export async function getMCPTools(agentDir, agentConfig) {
102
102
  },
103
103
  }), `${name} (${transport} ${server.url})`);
104
104
  } else {
105
- // Local MCP servers via stdio transport
106
- client = await withTimeout(createMCPClient({
107
- transport: {
108
- type: 'stdio',
109
- command: server.command,
110
- args: server.args || [],
111
- env: { ...process.env, ...resolveEnvVars(server.env || {}) },
112
- },
113
- }), `${name} (stdio ${server.command})`);
105
+ // Local MCP servers via stdio transport.
106
+ //
107
+ // @ai-sdk/mcp v1 doesn't accept a `{ type: 'stdio', ... }` config —
108
+ // its MCPTransportConfig is HTTP-only (`'sse' | 'http'`). For stdio
109
+ // you must instantiate the transport class explicitly from the
110
+ // `/mcp-stdio` subpath and pass the instance.
111
+ const { Experimental_StdioMCPTransport } =
112
+ await import('@ai-sdk/mcp/mcp-stdio');
113
+ const stdioTransport = new Experimental_StdioMCPTransport({
114
+ command: server.command,
115
+ args: server.args || [],
116
+ env: { ...process.env, ...resolveEnvVars(server.env || {}) },
117
+ });
118
+ client = await withTimeout(
119
+ createMCPClient({ transport: stdioTransport }),
120
+ `${name} (stdio ${server.command})`,
121
+ );
114
122
  }
115
123
 
116
124
  activeClients.push(client);
@@ -4,6 +4,7 @@ import { villageSpinner, brand } from '../utils/brand.js';
4
4
  import inquirer from 'inquirer';
5
5
  import { existsSync, readFileSync } from 'fs';
6
6
  import { join } from 'path';
7
+ import { homedir } from 'os';
7
8
  import { execSync } from 'child_process';
8
9
  import { isAuthenticated, getAccessToken } from '../utils/auth.js';
9
10
  import { getConfig, setConfig } from '../utils/config.js';
@@ -12,6 +13,7 @@ import {
12
13
  agentJoinCommunity as apiAgentJoinCommunity,
13
14
  listCommunities,
14
15
  getAgentActivity,
16
+ listMyAgents,
15
17
  listMyUnlinkedAgentProfiles,
16
18
  createVillageAgent as apiCreateVillageAgent,
17
19
  listAgentTasks,
@@ -41,7 +43,7 @@ import {
41
43
  readAgentLogs,
42
44
  getLogFilePath,
43
45
  } from '../utils/local-agent.js';
44
- import { scaffoldAgent, TOOL_CATALOG } from '../utils/agent-scaffolder.js';
46
+ import { scaffoldAgent, TOOL_CATALOG, resolveCatalogEntry } from '../utils/agent-scaffolder.js';
45
47
  import { formatLocalAgentList, formatLocalAgentStatus } from '../utils/formatters.js';
46
48
 
47
49
  // ── Create Local Agent (wizard) ─────────────────────────
@@ -117,11 +119,11 @@ export async function agentCreateLocalCommand() {
117
119
  message: 'Which tools should your agent have?',
118
120
  choices: [
119
121
  { name: 'MyVillageOS MCP (feed, posts, communities, wallet)', value: 'myvillage', checked: true, disabled: 'always enabled' },
120
- { name: 'Local Files (read/write project files)', value: 'filesystem', checked: true },
122
+ { name: 'Local Files (read/write a sandboxed workspace)', value: 'filesystem', checked: true },
121
123
  { name: 'Gmail', value: 'gmail' },
122
124
  { name: 'Calendar', value: 'calendar' },
123
125
  { name: 'GitHub', value: 'github' },
124
- { name: 'Browser', value: 'browser' },
126
+ { name: 'Browser (Puppeteer — downloads Chromium on first run)', value: 'browser' },
125
127
  ],
126
128
  },
127
129
  {
@@ -230,6 +232,26 @@ export async function agentStartCommand(name) {
230
232
  }
231
233
 
232
234
  if (!agentExists(name)) {
235
+ // Check whether the agent exists server-side under this handle. If so,
236
+ // this is a "different machine" situation \u2014 point them at `agent pull`
237
+ // rather than the misleading "not found" dead-end.
238
+ let remoteMatch = null;
239
+ try {
240
+ remoteMatch = await fetchMyAgentByHandle(name);
241
+ } catch {
242
+ // network error \u2014 fall through to the generic message below
243
+ }
244
+
245
+ if (remoteMatch) {
246
+ console.log(chalk.yellow(
247
+ ` Agent "${name}" exists on the server but isn't set up on this machine yet.`,
248
+ ));
249
+ console.log(chalk.yellow(
250
+ ` Run ${brand.gold(`myvillage agent pull ${name}`)} to scaffold it locally, then start it.\n`,
251
+ ));
252
+ return;
253
+ }
254
+
233
255
  console.log(chalk.red(` \u2717 Agent "${name}" not found. Run 'myvillage agent' to see your agents.\n`));
234
256
  return;
235
257
  }
@@ -396,7 +418,7 @@ export async function agentStartCommand(name) {
396
418
  const toolsConfig = readToolsYaml(name);
397
419
  if (toolsConfig.servers?.['man-feed'] && !toolsConfig.servers?.['myvillage']) {
398
420
  delete toolsConfig.servers['man-feed'];
399
- toolsConfig.servers = { myvillage: TOOL_CATALOG['myvillage'], ...toolsConfig.servers };
421
+ toolsConfig.servers = { myvillage: resolveCatalogEntry('myvillage', name), ...toolsConfig.servers };
400
422
  writeToolsYaml(name, toolsConfig);
401
423
  }
402
424
 
@@ -648,9 +670,22 @@ function printLogEntry(entry) {
648
670
  console.log(` ${' '.repeat(10)} ${brand.teal(`Tokens: ${entry.tokensUsed.prompt || 0}p / ${entry.tokensUsed.completion || 0}c`)}`);
649
671
  }
650
672
  break;
651
- case 'tool_call':
652
- console.log(` ${time} ${chalk.yellow('TOOL')} ${entry.tool || '?'} ${brand.teal(`→ ${entry.result || 'ok'}`)}`);
673
+ case 'tool_call': {
674
+ const arrow = entry.ok === false ? chalk.red('') : brand.teal('');
675
+ const statusLabel = entry.ok === false ? chalk.red('error') : '';
676
+ console.log(` ${time} ${chalk.yellow('TOOL')} ${entry.tool || '?'} ${arrow} ${statusLabel}`);
677
+ // Show args (if any) and the result content on indented sub-lines,
678
+ // so a developer tailing logs can actually see what the agent did.
679
+ if (entry.args && Object.keys(entry.args).length > 0) {
680
+ const argsStr = truncateLog(JSON.stringify(entry.args), 200);
681
+ console.log(` ${' '.repeat(10)} ${brand.teal('args:')} ${argsStr}`);
682
+ }
683
+ if (entry.result && entry.result !== 'ok' && entry.result !== 'executed') {
684
+ const resultStr = truncateLog(String(entry.result), 200);
685
+ console.log(` ${' '.repeat(10)} ${brand.teal('result:')} ${resultStr}`);
686
+ }
653
687
  break;
688
+ }
654
689
  case 'error':
655
690
  console.log(` ${time} ${chalk.red('ERR')} ${entry.error || entry.message || 'Unknown error'}`);
656
691
  break;
@@ -710,11 +745,15 @@ export async function agentAddToolCommand(name, tool) {
710
745
  }
711
746
 
712
747
  toolsConfig.servers = toolsConfig.servers || {};
713
- toolsConfig.servers[tool] = { ...TOOL_CATALOG[tool] };
748
+ toolsConfig.servers[tool] = resolveCatalogEntry(tool, name);
714
749
  writeToolsYaml(name, toolsConfig);
715
750
 
716
751
  console.log(brand.green(` \u2713 Added "${tool}" to agent "${name}".`));
717
752
 
753
+ if (tool === 'filesystem') {
754
+ const workspace = join(homedir(), '.myvillage', 'agents', name, 'workspace');
755
+ console.log(brand.teal(` Workspace: ${workspace}`));
756
+ }
718
757
  if (tool === 'github') {
719
758
  console.log(brand.teal(' Note: Set GITHUB_TOKEN in your environment for GitHub access.'));
720
759
  }
@@ -1091,3 +1130,111 @@ export async function agentRememberCommand(name, text, options = {}) {
1091
1130
  console.log(chalk.red(` ✗ Remember failed: ${msg}\n`));
1092
1131
  }
1093
1132
  }
1133
+
1134
+ // ── Pull Agent ─────────────────────────────────────────
1135
+ //
1136
+ // Rehydrate a local agent directory on a new machine from the server's
1137
+ // AgentProfile + VillageAgent records. Use case: you created `teacher_mvp`
1138
+ // on your laptop, now you want to run it on your desktop. The server
1139
+ // already has the agent — this command scaffolds the missing local config
1140
+ // (~/.myvillage/agents/<handle>/) so `agent start` can launch the daemon.
1141
+ //
1142
+ // What pulls down: the network identity (id, handle, displayName) and a
1143
+ // link to the existing VillageAgent task queue.
1144
+ // What doesn't: custom prompt edits, tools.yaml customizations, and any
1145
+ // API keys. Those stay machine-local and you'll need to redo them if you
1146
+ // want an exact replica. The scaffold defaults are a reasonable starting
1147
+ // point that will boot.
1148
+
1149
+ async function fetchMyAgentByHandle(handle) {
1150
+ const result = await listMyAgents({ includeVillageAgent: true });
1151
+ const agents = result.data || result;
1152
+ if (!Array.isArray(agents)) return null;
1153
+ return agents.find(a => a.handle === handle) || null;
1154
+ }
1155
+
1156
+ export async function agentPullCommand(handle, options = {}) {
1157
+ if (!isAuthenticated()) {
1158
+ console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
1159
+ return;
1160
+ }
1161
+
1162
+ // The local agent dir is keyed off the handle (= server-side AgentProfile.handle).
1163
+ const agentName = handle;
1164
+ const force = options.force === true;
1165
+
1166
+ if (agentExists(agentName) && !force) {
1167
+ console.log(chalk.yellow(
1168
+ ` Agent "${agentName}" already exists locally at ~/.myvillage/agents/${agentName}/`,
1169
+ ));
1170
+ console.log(chalk.yellow(' Use --force to overwrite, or just `myvillage agent start ' + agentName + '` to run it.\n'));
1171
+ return;
1172
+ }
1173
+
1174
+ const spinner = villageSpinner(`Pulling ${handle} from the network...`).start();
1175
+
1176
+ let profile;
1177
+ try {
1178
+ profile = await fetchMyAgentByHandle(handle);
1179
+ } catch (err) {
1180
+ spinner.fail(`Failed to look up agent: ${err.response?.data?.error || err.message}`);
1181
+ return;
1182
+ }
1183
+
1184
+ if (!profile) {
1185
+ spinner.fail(`No agent with handle "${handle}" is owned by your account.`);
1186
+ console.log(chalk.yellow(
1187
+ ' Run `myvillage agent` to see the agents on your account, then pull one by its handle.\n',
1188
+ ));
1189
+ return;
1190
+ }
1191
+
1192
+ // Default scaffold: just the always-on MyVillage MCP tool. Users can add
1193
+ // more via `agent add-tool` once they're up.
1194
+ const agentDir = getAgentDir(agentName);
1195
+ try {
1196
+ scaffoldAgent(agentDir, {
1197
+ name: agentName,
1198
+ displayName: profile.displayName || agentName,
1199
+ description: profile.bio || `Pulled from the network on ${new Date().toISOString().split('T')[0]}.`,
1200
+ tools: ['myvillage'],
1201
+ checkInInterval: 'hourly',
1202
+ provider: 'anthropic',
1203
+ model: 'claude-sonnet-4-5-20250929',
1204
+ });
1205
+ } catch (err) {
1206
+ spinner.fail(`Failed to scaffold local agent: ${err.message}`);
1207
+ return;
1208
+ }
1209
+
1210
+ // Prefill man.agent_id and man.village_agent_id so `start` skips the
1211
+ // create-on-first-boot step and links straight to the existing server rows.
1212
+ const cfg = readAgentConfig(agentName);
1213
+ cfg.man = cfg.man || {};
1214
+ cfg.man.agent_id = profile.id;
1215
+ cfg.man.handle = profile.handle;
1216
+ if (profile.villageAgent?.id) {
1217
+ cfg.man.village_agent_id = profile.villageAgent.id;
1218
+ }
1219
+ writeAgentConfig(agentName, cfg);
1220
+
1221
+ spinner.succeed(`Pulled ${handle} to ~/.myvillage/agents/${agentName}/`);
1222
+
1223
+ console.log('');
1224
+ console.log(brand.teal(' What pulled down:'));
1225
+ console.log(` - Network identity: @${profile.handle} (${profile.displayName})`);
1226
+ if (profile.villageAgent?.id) {
1227
+ console.log(` - VillageAgent task queue: ${profile.villageAgent.id}`);
1228
+ } else {
1229
+ console.log(` - ${chalk.yellow('No VillageAgent shim found — one will be created on first start.')}`);
1230
+ }
1231
+ console.log('');
1232
+ console.log(brand.teal(' Defaults that may need tweaking:'));
1233
+ console.log(` - ${brand.gold('prompt.md')} (system prompt — generic template)`);
1234
+ console.log(` - ${brand.gold('tools.yaml')} (only the MyVillage tool is enabled)`);
1235
+ console.log(` - ${brand.gold('agent.config.yaml')} (anthropic + claude-sonnet by default)`);
1236
+ console.log('');
1237
+ console.log(brand.teal(' Next:'));
1238
+ console.log(` ${brand.gold(`myvillage agent edit ${agentName}`)} Customize prompt / tools`);
1239
+ console.log(` ${brand.gold(`myvillage agent start ${agentName}`)} Launch the daemon\n`);
1240
+ }
package/src/index.js CHANGED
@@ -55,6 +55,7 @@ import {
55
55
  agentStopCommand,
56
56
  agentStatusCommand,
57
57
  agentLogsCommand,
58
+ agentPullCommand,
58
59
  agentAddToolCommand,
59
60
  agentRemoveToolCommand,
60
61
  agentTaskListCommand,
@@ -78,13 +79,6 @@ import {
78
79
  agentDeactivateClientCommand,
79
80
  agentRotateClientKeyCommand,
80
81
  } from './commands/agent-client.js';
81
- import {
82
- bizreqsNewCommand,
83
- bizreqsSpecCommand,
84
- bizreqsListCommand,
85
- bizreqsStatusCommand,
86
- bizreqsImportCommand,
87
- } from './commands/bizreqs.js';
88
82
  import {
89
83
  gameUpdateCommand,
90
84
  gameUploadThumbnailCommand,
@@ -447,6 +441,12 @@ export function run() {
447
441
  .description('Start a local agent daemon')
448
442
  .action(agentStartCommand);
449
443
 
444
+ agentCmd
445
+ .command('pull <handle>')
446
+ .description('Rehydrate a local agent dir on a new machine from the server')
447
+ .option('--force', 'Overwrite an existing local agent dir')
448
+ .action(agentPullCommand);
449
+
450
450
  agentCmd
451
451
  .command('stop <name>')
452
452
  .description('Stop a local agent daemon')
@@ -645,55 +645,6 @@ export function run() {
645
645
  .description('Show status of a reel draft')
646
646
  .action(mediaDraftStatusCommand);
647
647
 
648
- // ── BizReqs: Business Requirements Pipeline ───────────
649
-
650
- const bizreqsCmd = program
651
- .command('bizreqs')
652
- .description('Business requirements intake and project pipeline');
653
-
654
- bizreqsCmd
655
- .command('new')
656
- .description('Start a new AI-guided business intake session')
657
- .option('--org <name>', 'Organization name')
658
- .option('--contact <name>', 'Contact name')
659
- .option('--from-file <path>', 'Path to a text file with initial context')
660
- .option('--from-url <url>', 'URL to the organization\'s website')
661
- .option('--quick', 'Shortened flow (skip dream state, fewer exchanges)')
662
- .action(bizreqsNewCommand);
663
-
664
- bizreqsCmd
665
- .command('spec <id>')
666
- .description('Generate full project specification from an intake')
667
- .option('--detail <level>', 'Detail level: brief, standard, comprehensive', 'standard')
668
- .option('--output <dir>', 'Output directory for spec file', './specs')
669
- .action(bizreqsSpecCommand);
670
-
671
- bizreqsCmd
672
- .command('list')
673
- .description('List pipeline submissions')
674
- .option('--status <status>', 'Filter: new, in-review, spec-ready, assigned, in-progress, delivered')
675
- .option('--city <city>', 'Filter by city')
676
- .option('--search <query>', 'Search by org name, solution, or ID')
677
- .option('--sort <sort>', 'Sort: newest, oldest, priority', 'newest')
678
- .option('-n, --limit <number>', 'Number of results', '50')
679
- .option('--offset <number>', 'Pagination offset', '0')
680
- .option('--json', 'Output raw JSON')
681
- .action(bizreqsListCommand);
682
-
683
- bizreqsCmd
684
- .command('status <id>')
685
- .description('Check project status')
686
- .action(bizreqsStatusCommand);
687
-
688
- bizreqsCmd
689
- .command('import')
690
- .description('Import requirements from a file or URL')
691
- .option('--file <path>', 'Path to .txt or .md file')
692
- .option('--url <url>', 'URL to fetch content from')
693
- .option('--org <name>', 'Organization name')
694
- .option('--contact <name>', 'Contact name')
695
- .action(bizreqsImportCommand);
696
-
697
648
  // ── SoulPrint Studio: Model Training Pipeline ───────────
698
649
 
699
650
  // \u2500\u2500 Wisdom: agent skill packs (Books of Wisdom) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
@@ -1,8 +1,13 @@
1
- import { mkdirSync, writeFileSync } from 'fs';
1
+ import { mkdirSync, writeFileSync, existsSync } from 'fs';
2
2
  import { join } from 'path';
3
+ import { homedir } from 'os';
3
4
  import { stringify as stringifyYaml } from 'yaml';
4
5
 
5
6
  // ── MCP Tool Catalog ────────────────────────────────────
7
+ //
8
+ // Static-shape entries. The `filesystem` entry's allow-listed root is
9
+ // resolved per-agent at scaffold/add-tool time — see
10
+ // resolveCatalogEntry() — so each agent gets its own sandboxed workspace.
6
11
 
7
12
  const TOOL_CATALOG = {
8
13
  myvillage: {
@@ -12,8 +17,10 @@ const TOOL_CATALOG = {
12
17
  },
13
18
  filesystem: {
14
19
  command: 'npx',
15
- args: ['-y', '@modelcontextprotocol/server-filesystem', '~/projects'],
16
- description: 'Read/write local project files',
20
+ // Last arg is a placeholder; replaced with `<agentDir>/workspace`
21
+ // by resolveCatalogEntry().
22
+ args: ['-y', '@modelcontextprotocol/server-filesystem', '__AGENT_WORKSPACE__'],
23
+ description: 'Read/write files under the agent\'s sandboxed workspace',
17
24
  },
18
25
  gmail: {
19
26
  command: 'npx',
@@ -33,13 +40,41 @@ const TOOL_CATALOG = {
33
40
  },
34
41
  browser: {
35
42
  command: 'npx',
36
- args: ['-y', '@anthropic/mcp-puppeteer'],
37
- description: 'Navigate web pages, extract content',
43
+ args: ['-y', '@modelcontextprotocol/server-puppeteer'],
44
+ description: 'Navigate web pages, extract content (downloads Chromium on first run)',
38
45
  },
39
46
  };
40
47
 
41
48
  export { TOOL_CATALOG };
42
49
 
50
+ // ── Catalog Resolution ──────────────────────────────────
51
+
52
+ /**
53
+ * Returns a deep-copied catalog entry with per-agent placeholders
54
+ * resolved (e.g. the filesystem workspace path). Also creates any
55
+ * directories the entry depends on (the workspace dir for filesystem).
56
+ *
57
+ * Use this anywhere you'd otherwise do `{ ...TOOL_CATALOG[toolId] }`
58
+ * to write into a tools.yaml.
59
+ */
60
+ export function resolveCatalogEntry(toolId, agentName) {
61
+ const entry = TOOL_CATALOG[toolId];
62
+ if (!entry) return null;
63
+ const resolved = JSON.parse(JSON.stringify(entry));
64
+
65
+ if (toolId === 'filesystem') {
66
+ const workspace = join(homedir(), '.myvillage', 'agents', agentName, 'workspace');
67
+ if (!existsSync(workspace)) {
68
+ mkdirSync(workspace, { recursive: true });
69
+ }
70
+ resolved.args = resolved.args.map(a =>
71
+ a === '__AGENT_WORKSPACE__' ? workspace : a,
72
+ );
73
+ }
74
+
75
+ return resolved;
76
+ }
77
+
43
78
  // ── Check-in Interval Map ───────────────────────────────
44
79
 
45
80
  const INTERVAL_MAP = {
@@ -73,7 +108,7 @@ export function scaffoldAgent(agentDir, options) {
73
108
  // Write tools.yaml
74
109
  writeFileSync(
75
110
  join(agentDir, 'tools.yaml'),
76
- generateToolsYaml(tools)
111
+ generateToolsYaml(tools, name)
77
112
  );
78
113
 
79
114
  // Write logs/.gitkeep
@@ -156,17 +191,16 @@ When you receive a TASK in your context, follow these rules:
156
191
 
157
192
  // ── Tools YAML Generation ───────────────────────────────
158
193
 
159
- function generateToolsYaml(selectedTools) {
194
+ function generateToolsYaml(selectedTools, agentName) {
160
195
  const servers = {};
161
196
 
162
197
  // myvillage is always included
163
- servers['myvillage'] = TOOL_CATALOG['myvillage'];
198
+ servers['myvillage'] = resolveCatalogEntry('myvillage', agentName);
164
199
 
165
200
  for (const toolId of selectedTools) {
166
201
  if (toolId === 'myvillage') continue;
167
- if (TOOL_CATALOG[toolId]) {
168
- servers[toolId] = { ...TOOL_CATALOG[toolId] };
169
- }
202
+ const entry = resolveCatalogEntry(toolId, agentName);
203
+ if (entry) servers[toolId] = entry;
170
204
  }
171
205
 
172
206
  return stringifyYaml({ servers }, { lineWidth: 0 });