@myvillage/cli 1.26.0 → 1.28.1

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.28.1",
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';
@@ -41,7 +42,7 @@ import {
41
42
  readAgentLogs,
42
43
  getLogFilePath,
43
44
  } from '../utils/local-agent.js';
44
- import { scaffoldAgent, TOOL_CATALOG } from '../utils/agent-scaffolder.js';
45
+ import { scaffoldAgent, TOOL_CATALOG, resolveCatalogEntry } from '../utils/agent-scaffolder.js';
45
46
  import { formatLocalAgentList, formatLocalAgentStatus } from '../utils/formatters.js';
46
47
 
47
48
  // ── Create Local Agent (wizard) ─────────────────────────
@@ -117,11 +118,10 @@ export async function agentCreateLocalCommand() {
117
118
  message: 'Which tools should your agent have?',
118
119
  choices: [
119
120
  { 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 },
121
+ { name: 'Local Files (read/write a sandboxed workspace)', value: 'filesystem', checked: true },
121
122
  { name: 'Gmail', value: 'gmail' },
122
123
  { name: 'Calendar', value: 'calendar' },
123
124
  { name: 'GitHub', value: 'github' },
124
- { name: 'Browser', value: 'browser' },
125
125
  ],
126
126
  },
127
127
  {
@@ -396,7 +396,7 @@ export async function agentStartCommand(name) {
396
396
  const toolsConfig = readToolsYaml(name);
397
397
  if (toolsConfig.servers?.['man-feed'] && !toolsConfig.servers?.['myvillage']) {
398
398
  delete toolsConfig.servers['man-feed'];
399
- toolsConfig.servers = { myvillage: TOOL_CATALOG['myvillage'], ...toolsConfig.servers };
399
+ toolsConfig.servers = { myvillage: resolveCatalogEntry('myvillage', name), ...toolsConfig.servers };
400
400
  writeToolsYaml(name, toolsConfig);
401
401
  }
402
402
 
@@ -648,9 +648,22 @@ function printLogEntry(entry) {
648
648
  console.log(` ${' '.repeat(10)} ${brand.teal(`Tokens: ${entry.tokensUsed.prompt || 0}p / ${entry.tokensUsed.completion || 0}c`)}`);
649
649
  }
650
650
  break;
651
- case 'tool_call':
652
- console.log(` ${time} ${chalk.yellow('TOOL')} ${entry.tool || '?'} ${brand.teal(`→ ${entry.result || 'ok'}`)}`);
651
+ case 'tool_call': {
652
+ const arrow = entry.ok === false ? chalk.red('') : brand.teal('');
653
+ const statusLabel = entry.ok === false ? chalk.red('error') : '';
654
+ console.log(` ${time} ${chalk.yellow('TOOL')} ${entry.tool || '?'} ${arrow} ${statusLabel}`);
655
+ // Show args (if any) and the result content on indented sub-lines,
656
+ // so a developer tailing logs can actually see what the agent did.
657
+ if (entry.args && Object.keys(entry.args).length > 0) {
658
+ const argsStr = truncateLog(JSON.stringify(entry.args), 200);
659
+ console.log(` ${' '.repeat(10)} ${brand.teal('args:')} ${argsStr}`);
660
+ }
661
+ if (entry.result && entry.result !== 'ok' && entry.result !== 'executed') {
662
+ const resultStr = truncateLog(String(entry.result), 200);
663
+ console.log(` ${' '.repeat(10)} ${brand.teal('result:')} ${resultStr}`);
664
+ }
653
665
  break;
666
+ }
654
667
  case 'error':
655
668
  console.log(` ${time} ${chalk.red('ERR')} ${entry.error || entry.message || 'Unknown error'}`);
656
669
  break;
@@ -710,11 +723,15 @@ export async function agentAddToolCommand(name, tool) {
710
723
  }
711
724
 
712
725
  toolsConfig.servers = toolsConfig.servers || {};
713
- toolsConfig.servers[tool] = { ...TOOL_CATALOG[tool] };
726
+ toolsConfig.servers[tool] = resolveCatalogEntry(tool, name);
714
727
  writeToolsYaml(name, toolsConfig);
715
728
 
716
729
  console.log(brand.green(` \u2713 Added "${tool}" to agent "${name}".`));
717
730
 
731
+ if (tool === 'filesystem') {
732
+ const workspace = join(homedir(), '.myvillage', 'agents', name, 'workspace');
733
+ console.log(brand.teal(` Workspace: ${workspace}`));
734
+ }
718
735
  if (tool === 'github') {
719
736
  console.log(brand.teal(' Note: Set GITHUB_TOKEN in your environment for GitHub access.'));
720
737
  }
@@ -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',
@@ -31,15 +38,41 @@ const TOOL_CATALOG = {
31
38
  env: { GITHUB_TOKEN: '${GITHUB_TOKEN}' },
32
39
  description: 'GitHub repository access',
33
40
  },
34
- browser: {
35
- command: 'npx',
36
- args: ['-y', '@anthropic/mcp-puppeteer'],
37
- description: 'Navigate web pages, extract content',
38
- },
41
+ // browser: removed in v1.27.0 — the previously catalog'd
42
+ // `@anthropic/mcp-puppeteer` package is no longer published under that
43
+ // name. Add a working browser MCP back when one stabilizes.
39
44
  };
40
45
 
41
46
  export { TOOL_CATALOG };
42
47
 
48
+ // ── Catalog Resolution ──────────────────────────────────
49
+
50
+ /**
51
+ * Returns a deep-copied catalog entry with per-agent placeholders
52
+ * resolved (e.g. the filesystem workspace path). Also creates any
53
+ * directories the entry depends on (the workspace dir for filesystem).
54
+ *
55
+ * Use this anywhere you'd otherwise do `{ ...TOOL_CATALOG[toolId] }`
56
+ * to write into a tools.yaml.
57
+ */
58
+ export function resolveCatalogEntry(toolId, agentName) {
59
+ const entry = TOOL_CATALOG[toolId];
60
+ if (!entry) return null;
61
+ const resolved = JSON.parse(JSON.stringify(entry));
62
+
63
+ if (toolId === 'filesystem') {
64
+ const workspace = join(homedir(), '.myvillage', 'agents', agentName, 'workspace');
65
+ if (!existsSync(workspace)) {
66
+ mkdirSync(workspace, { recursive: true });
67
+ }
68
+ resolved.args = resolved.args.map(a =>
69
+ a === '__AGENT_WORKSPACE__' ? workspace : a,
70
+ );
71
+ }
72
+
73
+ return resolved;
74
+ }
75
+
43
76
  // ── Check-in Interval Map ───────────────────────────────
44
77
 
45
78
  const INTERVAL_MAP = {
@@ -73,7 +106,7 @@ export function scaffoldAgent(agentDir, options) {
73
106
  // Write tools.yaml
74
107
  writeFileSync(
75
108
  join(agentDir, 'tools.yaml'),
76
- generateToolsYaml(tools)
109
+ generateToolsYaml(tools, name)
77
110
  );
78
111
 
79
112
  // Write logs/.gitkeep
@@ -156,17 +189,16 @@ When you receive a TASK in your context, follow these rules:
156
189
 
157
190
  // ── Tools YAML Generation ───────────────────────────────
158
191
 
159
- function generateToolsYaml(selectedTools) {
192
+ function generateToolsYaml(selectedTools, agentName) {
160
193
  const servers = {};
161
194
 
162
195
  // myvillage is always included
163
- servers['myvillage'] = TOOL_CATALOG['myvillage'];
196
+ servers['myvillage'] = resolveCatalogEntry('myvillage', agentName);
164
197
 
165
198
  for (const toolId of selectedTools) {
166
199
  if (toolId === 'myvillage') continue;
167
- if (TOOL_CATALOG[toolId]) {
168
- servers[toolId] = { ...TOOL_CATALOG[toolId] };
169
- }
200
+ const entry = resolveCatalogEntry(toolId, agentName);
201
+ if (entry) servers[toolId] = entry;
170
202
  }
171
203
 
172
204
  return stringifyYaml({ servers }, { lineWidth: 0 });