@myvillage/cli 1.5.0 → 1.6.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.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "MyVillageOS CLI for community developers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,16 +26,17 @@
26
26
  "author": "MyVillage Project",
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
- "commander": "^11.1.0",
30
- "inquirer": "^9.2.12",
29
+ "@ai-sdk/anthropic": "^1.0.0",
30
+ "ai": "^4.0.0",
31
31
  "axios": "^1.6.2",
32
32
  "chalk": "^5.3.0",
33
- "ora": "^8.0.1",
34
- "open": "^10.0.3",
33
+ "commander": "^11.1.0",
35
34
  "conf": "^12.0.0",
35
+ "inquirer": "^9.2.12",
36
+ "open": "^10.0.3",
37
+ "ora": "^8.0.1",
38
+ "p-limit": "^5.0.0",
36
39
  "update-notifier": "^7.0.0",
37
- "ai": "^4.0.0",
38
- "@ai-sdk/anthropic": "^1.0.0",
39
40
  "yaml": "^2.3.0",
40
41
  "zod": "^3.22.0"
41
42
  }
@@ -131,9 +131,9 @@ export async function agentLoop(agentName, { signal }) {
131
131
  if (step.toolCalls?.length) {
132
132
  for (const tc of step.toolCalls) {
133
133
  activity.toolCalls++;
134
- if (tc.toolName === 'man_create_post') activity.postsCreated++;
135
- if (tc.toolName === 'man_create_comment') activity.commentsCreated++;
136
- if (tc.toolName === 'man_vote') activity.votesGiven++;
134
+ if (tc.toolName === 'post_create') activity.postsCreated++;
135
+ if (tc.toolName === 'comment_create') activity.commentsCreated++;
136
+ if (tc.toolName === 'vote_cast') activity.votesGiven++;
137
137
  }
138
138
  }
139
139
  if (step.toolCalls?.length && step.toolResults?.length) {
@@ -160,9 +160,9 @@ export async function agentLoop(agentName, { signal }) {
160
160
  } else if (result.toolCalls?.length) {
161
161
  for (const tc of result.toolCalls) {
162
162
  activity.toolCalls++;
163
- if (tc.toolName === 'man_create_post') activity.postsCreated++;
164
- if (tc.toolName === 'man_create_comment') activity.commentsCreated++;
165
- if (tc.toolName === 'man_vote') activity.votesGiven++;
163
+ if (tc.toolName === 'post_create') activity.postsCreated++;
164
+ if (tc.toolName === 'comment_create') activity.commentsCreated++;
165
+ if (tc.toolName === 'vote_cast') activity.votesGiven++;
166
166
  logActivity(agentDir, {
167
167
  type: 'tool_call',
168
168
  tool: tc.toolName,
@@ -174,11 +174,11 @@ export async function agentLoop(agentName, { signal }) {
174
174
 
175
175
  // Record actions for dedup in future iterations
176
176
  const collectActions = (toolName, args) => {
177
- if (toolName === 'man_create_comment' && args?.postId) {
177
+ if (toolName === 'comment_create' && args?.postId) {
178
178
  recentActions.push({ type: 'comment', postId: args.postId, ts: new Date().toISOString() });
179
- } else if (toolName === 'man_create_post' && args?.communitySlug) {
179
+ } else if (toolName === 'post_create' && args?.communitySlug) {
180
180
  recentActions.push({ type: 'post', community: args.communitySlug, ts: new Date().toISOString() });
181
- } else if (toolName === 'man_vote' && args?.targetId) {
181
+ } else if (toolName === 'vote_cast' && args?.targetId) {
182
182
  recentActions.push({ type: 'vote', targetId: args.targetId, targetType: args.targetType, ts: new Date().toISOString() });
183
183
  }
184
184
  };
@@ -5,20 +5,6 @@
5
5
  import { readFileSync } from 'fs';
6
6
  import { join } from 'path';
7
7
  import { parse as parseYaml } from 'yaml';
8
- import { tool } from 'ai';
9
- import { z } from 'zod';
10
- import {
11
- getLatestFeed,
12
- createPost,
13
- createComment,
14
- castVote,
15
- searchNetwork,
16
- getAgentMentions,
17
- upsertAgentMemory,
18
- listAgentMemory,
19
- getAgentMemoryEntry,
20
- deleteAgentMemoryEntry,
21
- } from '../utils/api.js';
22
8
 
23
9
  const activeClients = [];
24
10
 
@@ -37,21 +23,42 @@ export async function getMCPTools(agentDir, agentConfig) {
37
23
 
38
24
  for (const [name, server] of Object.entries(toolsConfig.servers || {})) {
39
25
  if (server.command === 'internal') {
40
- Object.assign(allTools, getInternalTools(name, agentConfig));
26
+ // Legacy internal tools — skip (replaced by MCP server)
41
27
  continue;
42
28
  }
43
29
 
44
- // External MCP servers via stdio transport
45
30
  try {
46
31
  const { experimental_createMCPClient: createMCPClient } = await import('ai');
47
- const client = await createMCPClient({
48
- transport: {
49
- type: 'stdio',
50
- command: server.command,
51
- args: server.args || [],
52
- env: { ...process.env, ...resolveEnvVars(server.env || {}) },
53
- },
54
- });
32
+ let client;
33
+
34
+ if (server.url) {
35
+ // Remote MCP server via Streamable HTTP transport
36
+ const headers = {};
37
+ if (process.env.MYVILLAGE_ACCESS_TOKEN) {
38
+ headers['Authorization'] = `Bearer ${process.env.MYVILLAGE_ACCESS_TOKEN}`;
39
+ }
40
+ if (process.env.MYVILLAGE_AGENT_ID) {
41
+ headers['X-Agent-Id'] = process.env.MYVILLAGE_AGENT_ID;
42
+ }
43
+ client = await createMCPClient({
44
+ transport: {
45
+ type: 'sse',
46
+ url: server.url,
47
+ headers,
48
+ },
49
+ });
50
+ } else {
51
+ // Local MCP servers via stdio transport
52
+ client = await createMCPClient({
53
+ transport: {
54
+ type: 'stdio',
55
+ command: server.command,
56
+ args: server.args || [],
57
+ env: { ...process.env, ...resolveEnvVars(server.env || {}) },
58
+ },
59
+ });
60
+ }
61
+
55
62
  activeClients.push(client);
56
63
  const tools = await client.tools();
57
64
  Object.assign(allTools, tools);
@@ -71,320 +78,6 @@ export async function cleanupMCPClients() {
71
78
  activeClients.length = 0;
72
79
  }
73
80
 
74
- // ── Internal Tools (MAN Feed) ───────────────────────────
75
-
76
- function getInternalTools(name, agentConfig) {
77
- if (name !== 'man-feed') return {};
78
-
79
- const agentProfileId = agentConfig?.man?.agent_id || null;
80
-
81
- // Rate limit tracking (in-memory, resets on daemon restart)
82
- const counters = {
83
- postsToday: 0,
84
- apiCallsThisHour: 0,
85
- lastHourReset: Date.now(),
86
- lastDayReset: new Date().toDateString(),
87
- };
88
-
89
- function checkRateLimits(type) {
90
- const limits = agentConfig?.limits || {};
91
- const now = new Date();
92
-
93
- // Reset daily counter
94
- if (now.toDateString() !== counters.lastDayReset) {
95
- counters.postsToday = 0;
96
- counters.lastDayReset = now.toDateString();
97
- }
98
-
99
- // Reset hourly counter
100
- if (Date.now() - counters.lastHourReset > 3600000) {
101
- counters.apiCallsThisHour = 0;
102
- counters.lastHourReset = Date.now();
103
- }
104
-
105
- if (type === 'post' && counters.postsToday >= (limits.max_posts_per_day || 10)) {
106
- return 'Rate limit: max posts per day reached';
107
- }
108
-
109
- if (counters.apiCallsThisHour >= (limits.max_api_calls_per_hour || 20)) {
110
- return 'Rate limit: max API calls per hour reached';
111
- }
112
-
113
- return null;
114
- }
115
-
116
- return {
117
- man_read_feed: tool({
118
- description: 'Read recent posts from the MAN feed. Returns the latest posts from communities.',
119
- parameters: z.object({
120
- limit: z.number().optional().describe('Number of posts to fetch (default 20)'),
121
- community: z.string().optional().describe('Filter by community slug'),
122
- }),
123
- execute: async ({ limit, community }) => {
124
- const rateErr = checkRateLimits('read');
125
- if (rateErr) return { error: rateErr };
126
- counters.apiCallsThisHour++;
127
-
128
- try {
129
- const params = { pageSize: limit || 20 };
130
- if (community) params.communitySlug = community;
131
- const result = await getLatestFeed(params);
132
- const items = result.data || result;
133
- if (!Array.isArray(items)) return { posts: [] };
134
- return {
135
- posts: items.map(p => ({
136
- id: p.id,
137
- title: p.title,
138
- body: (p.body || '').slice(0, 200),
139
- author: p.agentProfile?.handle || p.villager?.firstName || 'unknown',
140
- community: p.community?.slug,
141
- upvotes: p.upvoteCount || 0,
142
- comments: p.commentCount || 0,
143
- createdAt: p.createdAt,
144
- })),
145
- };
146
- } catch (err) {
147
- return { error: err.message };
148
- }
149
- },
150
- }),
151
-
152
- man_create_post: tool({
153
- description: 'Create a new post on the MAN feed as this agent.',
154
- parameters: z.object({
155
- communitySlug: z.string().describe('Community slug to post in'),
156
- body: z.string().describe('Post content'),
157
- title: z.string().optional().describe('Post title'),
158
- postType: z.enum(['DISCUSSION', 'QUESTION', 'PROJECT_SHOWCASE', 'TUTORIAL']).optional().describe('Post type (default DISCUSSION)'),
159
- }),
160
- execute: async ({ communitySlug, body, title, postType }) => {
161
- if (!agentProfileId) return { error: 'Agent not registered on MAN' };
162
- const rateErr = checkRateLimits('post');
163
- if (rateErr) return { error: rateErr };
164
- counters.apiCallsThisHour++;
165
- counters.postsToday++;
166
-
167
- try {
168
- const data = {
169
- communitySlug,
170
- body,
171
- postType: postType || 'DISCUSSION',
172
- agentProfileId,
173
- };
174
- if (title) data.title = title;
175
- const result = await createPost(data);
176
- const post = result.data || result;
177
- return { success: true, postId: post.id };
178
- } catch (err) {
179
- counters.postsToday--; // undo on failure
180
- return { error: err.response?.data?.message || err.message };
181
- }
182
- },
183
- }),
184
-
185
- man_create_comment: tool({
186
- description: 'Reply to a post on the MAN feed as this agent.',
187
- parameters: z.object({
188
- postId: z.string().describe('ID of the post to comment on'),
189
- body: z.string().describe('Comment content'),
190
- }),
191
- execute: async ({ postId, body }) => {
192
- if (!agentProfileId) return { error: 'Agent not registered on MAN' };
193
- const rateErr = checkRateLimits('comment');
194
- if (rateErr) return { error: rateErr };
195
- counters.apiCallsThisHour++;
196
-
197
- try {
198
- const result = await createComment(postId, { body, agentProfileId });
199
- const comment = result.data || result;
200
- return { success: true, commentId: comment.id };
201
- } catch (err) {
202
- return { error: err.response?.data?.message || err.message };
203
- }
204
- },
205
- }),
206
-
207
- man_vote: tool({
208
- description: 'Upvote a post or comment on the MAN feed.',
209
- parameters: z.object({
210
- targetType: z.enum(['POST', 'COMMENT']).describe('What to vote on'),
211
- targetId: z.string().describe('ID of the post or comment'),
212
- }),
213
- execute: async ({ targetType, targetId }) => {
214
- if (!agentProfileId) return { error: 'Agent not registered on MAN' };
215
- const rateErr = checkRateLimits('vote');
216
- if (rateErr) return { error: rateErr };
217
- counters.apiCallsThisHour++;
218
-
219
- try {
220
- await castVote({
221
- targetType,
222
- targetId,
223
- value: 'UP',
224
- agentProfileId,
225
- });
226
- return { success: true };
227
- } catch (err) {
228
- return { error: err.response?.data?.message || err.message };
229
- }
230
- },
231
- }),
232
-
233
- man_search: tool({
234
- description: 'Search the MAN feed for posts, communities, or users.',
235
- parameters: z.object({
236
- query: z.string().describe('Search query'),
237
- type: z.enum(['posts', 'communities', 'users']).optional().describe('Filter search by type'),
238
- }),
239
- execute: async ({ query, type }) => {
240
- const rateErr = checkRateLimits('search');
241
- if (rateErr) return { error: rateErr };
242
- counters.apiCallsThisHour++;
243
-
244
- try {
245
- const params = { q: query };
246
- if (type) params.type = type;
247
- const result = await searchNetwork(params);
248
- return result.data || result;
249
- } catch (err) {
250
- return { error: err.message };
251
- }
252
- },
253
- }),
254
-
255
- man_get_mentions: tool({
256
- description: 'Get posts and comments that mention this agent. Use to find conversations directed at you.',
257
- parameters: z.object({
258
- since: z.string().optional().describe('ISO 8601 timestamp to fetch mentions after'),
259
- limit: z.number().optional().describe('Number of mentions to fetch (default 20)'),
260
- }),
261
- execute: async ({ since, limit }) => {
262
- if (!agentProfileId) return { error: 'Agent not registered on MAN' };
263
- const rateErr = checkRateLimits('read');
264
- if (rateErr) return { error: rateErr };
265
- counters.apiCallsThisHour++;
266
-
267
- try {
268
- const params = { pageSize: limit || 20 };
269
- if (since) params.since = since;
270
- const result = await getAgentMentions(agentProfileId, params);
271
- const items = result.data || result;
272
- if (!Array.isArray(items)) return { mentions: [] };
273
- return {
274
- mentions: items.map(m => ({
275
- id: m.id,
276
- type: m.postId ? 'comment' : 'post',
277
- body: (m.body || '').slice(0, 200),
278
- author: m.agentProfile?.handle || m.villager?.firstName || 'unknown',
279
- postId: m.postId || m.id,
280
- createdAt: m.createdAt,
281
- })),
282
- };
283
- } catch (err) {
284
- return { error: err.message };
285
- }
286
- },
287
- }),
288
-
289
- man_memory_set: tool({
290
- description: 'Store a key-value pair in persistent memory. Use to remember things across sessions.',
291
- parameters: z.object({
292
- key: z.string().describe('Memory key (e.g., "preferred_communities", "strategy_notes")'),
293
- value: z.string().describe('Value to store (string or JSON-stringified object)'),
294
- category: z.string().optional().describe('Category for grouping (e.g., "preferences", "observations")'),
295
- }),
296
- execute: async ({ key, value, category }) => {
297
- if (!agentProfileId) return { error: 'Agent not registered on MAN' };
298
- const rateErr = checkRateLimits('memory');
299
- if (rateErr) return { error: rateErr };
300
- counters.apiCallsThisHour++;
301
-
302
- try {
303
- const data = { key, value };
304
- if (category) data.category = category;
305
- await upsertAgentMemory(agentProfileId, data);
306
- return { success: true, key };
307
- } catch (err) {
308
- return { error: err.response?.data?.message || err.message };
309
- }
310
- },
311
- }),
312
-
313
- man_memory_get: tool({
314
- description: 'Retrieve a specific memory entry by key.',
315
- parameters: z.object({
316
- key: z.string().describe('Memory key to retrieve'),
317
- }),
318
- execute: async ({ key }) => {
319
- if (!agentProfileId) return { error: 'Agent not registered on MAN' };
320
- const rateErr = checkRateLimits('read');
321
- if (rateErr) return { error: rateErr };
322
- counters.apiCallsThisHour++;
323
-
324
- try {
325
- const result = await getAgentMemoryEntry(agentProfileId, key);
326
- const entry = result.data || result;
327
- return { key: entry.key, value: entry.value, category: entry.category };
328
- } catch (err) {
329
- if (err.response?.status === 404) return { key, value: null, found: false };
330
- return { error: err.message };
331
- }
332
- },
333
- }),
334
-
335
- man_memory_list: tool({
336
- description: 'List all memory entries, optionally filtered by category.',
337
- parameters: z.object({
338
- category: z.string().optional().describe('Filter by category'),
339
- }),
340
- execute: async ({ category }) => {
341
- if (!agentProfileId) return { error: 'Agent not registered on MAN' };
342
- const rateErr = checkRateLimits('read');
343
- if (rateErr) return { error: rateErr };
344
- counters.apiCallsThisHour++;
345
-
346
- try {
347
- const params = {};
348
- if (category) params.category = category;
349
- const result = await listAgentMemory(agentProfileId, params);
350
- const entries = result.data || result;
351
- if (!Array.isArray(entries)) return { entries: [] };
352
- return {
353
- entries: entries.map(e => ({
354
- key: e.key,
355
- value: e.value,
356
- category: e.category,
357
- updatedAt: e.updatedAt,
358
- })),
359
- };
360
- } catch (err) {
361
- return { error: err.message };
362
- }
363
- },
364
- }),
365
-
366
- man_memory_delete: tool({
367
- description: 'Delete a memory entry by key.',
368
- parameters: z.object({
369
- key: z.string().describe('Memory key to delete'),
370
- }),
371
- execute: async ({ key }) => {
372
- if (!agentProfileId) return { error: 'Agent not registered on MAN' };
373
- const rateErr = checkRateLimits('memory');
374
- if (rateErr) return { error: rateErr };
375
- counters.apiCallsThisHour++;
376
-
377
- try {
378
- await deleteAgentMemoryEntry(agentProfileId, key);
379
- return { success: true, key };
380
- } catch (err) {
381
- return { error: err.response?.data?.message || err.message };
382
- }
383
- },
384
- }),
385
- };
386
- }
387
-
388
81
  // ── Helpers ─────────────────────────────────────────────
389
82
 
390
83
  function resolveEnvVars(envMap) {
@@ -5,7 +5,7 @@ import inquirer from 'inquirer';
5
5
  import { existsSync, readFileSync } from 'fs';
6
6
  import { join } from 'path';
7
7
  import { execSync } from 'child_process';
8
- import { isAuthenticated } from '../utils/auth.js';
8
+ import { isAuthenticated, getAccessToken } from '../utils/auth.js';
9
9
  import { getConfig, setConfig } from '../utils/config.js';
10
10
  import {
11
11
  createAgent as apiCreateAgent,
@@ -72,7 +72,7 @@ export async function agentCreateLocalCommand() {
72
72
  name: 'tools',
73
73
  message: 'Which tools should your agent have?',
74
74
  choices: [
75
- { name: 'MAN Feed (post, read, react)', value: 'man-feed', checked: true, disabled: 'always enabled' },
75
+ { name: 'MyVillageOS MCP (feed, posts, communities, wallet)', value: 'myvillage', checked: true, disabled: 'always enabled' },
76
76
  { name: 'Local Files (read/write project files)', value: 'filesystem', checked: true },
77
77
  { name: 'Gmail', value: 'gmail' },
78
78
  { name: 'Calendar', value: 'calendar' },
@@ -109,8 +109,8 @@ export async function agentCreateLocalCommand() {
109
109
 
110
110
  const agentDir = getAgentDir(answers.name);
111
111
 
112
- // man-feed is always included even though disabled in checkbox
113
- const tools = ['man-feed', ...answers.tools.filter(t => t !== 'man-feed')];
112
+ // myvillage is always included even though disabled in checkbox
113
+ const tools = ['myvillage', ...answers.tools.filter(t => t !== 'myvillage')];
114
114
 
115
115
  scaffoldAgent(agentDir, {
116
116
  name: answers.name,
@@ -236,6 +236,23 @@ export async function agentStartCommand(name) {
236
236
  }
237
237
  }
238
238
 
239
+ // Migrate tools.yaml: replace man-feed with myvillage MCP server
240
+ const toolsConfig = readToolsYaml(name);
241
+ if (toolsConfig.servers?.['man-feed'] && !toolsConfig.servers?.['myvillage']) {
242
+ delete toolsConfig.servers['man-feed'];
243
+ toolsConfig.servers = { myvillage: TOOL_CATALOG['myvillage'], ...toolsConfig.servers };
244
+ writeToolsYaml(name, toolsConfig);
245
+ }
246
+
247
+ // Pass MyVillage credentials to daemon for MCP server
248
+ const accessToken = getAccessToken();
249
+ if (accessToken) {
250
+ process.env.MYVILLAGE_ACCESS_TOKEN = accessToken;
251
+ }
252
+ if (agentConfig.man?.agent_id) {
253
+ process.env.MYVILLAGE_AGENT_ID = agentConfig.man.agent_id;
254
+ }
255
+
239
256
  // Fork the daemon process
240
257
  const spinner = villageSpinner(`Starting agent "${name}"...`).start();
241
258
 
@@ -492,7 +509,7 @@ export async function agentAddToolCommand(name, tool) {
492
509
  return;
493
510
  }
494
511
 
495
- const validTools = Object.keys(TOOL_CATALOG).filter(t => t !== 'man-feed');
512
+ const validTools = Object.keys(TOOL_CATALOG).filter(t => t !== 'myvillage');
496
513
  if (!validTools.includes(tool)) {
497
514
  console.log(chalk.red(` \u2717 Unknown tool "${tool}".`));
498
515
  console.log(brand.teal(` Available tools: ${validTools.join(', ')}\n`));
@@ -530,8 +547,8 @@ export async function agentRemoveToolCommand(name, tool) {
530
547
  return;
531
548
  }
532
549
 
533
- if (tool === 'man-feed') {
534
- console.log(chalk.red(' \u2717 Cannot remove man-feed — it is always enabled.\n'));
550
+ if (tool === 'myvillage') {
551
+ console.log(chalk.red(' \u2717 Cannot remove myvillage — it is always enabled.\n'));
535
552
  return;
536
553
  }
537
554
 
@@ -100,31 +100,7 @@ const recommendationParams = z.object({
100
100
 
101
101
  // ── Helper: Get Anthropic client ────────────────────────
102
102
 
103
- async function getAnthropicProvider() {
104
- const { createAnthropic } = await import('@ai-sdk/anthropic');
105
- const config = getConfig();
106
- let apiKey = config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
107
-
108
- if (!apiKey) {
109
- console.log(chalk.yellow('\n Anthropic API key required for AI-powered intake.'));
110
- console.log(brand.teal(' Get yours at: https://console.anthropic.com\n'));
111
-
112
- const { key } = await inquirer.prompt([{
113
- type: 'password',
114
- name: 'key',
115
- message: 'Anthropic API key:',
116
- mask: '*',
117
- validate: (input) => input.trim().length > 0 || 'API key is required',
118
- }]);
119
-
120
- apiKey = key.trim();
121
- const { setConfig } = await import('../utils/config.js');
122
- setConfig({ anthropicApiKey: apiKey });
123
- console.log(brand.green(' ✓ API key saved to ~/.myvillage/config.json\n'));
124
- }
125
-
126
- return createAnthropic({ apiKey });
127
- }
103
+ import { getAnthropicProvider } from '../utils/ai.js';
128
104
 
129
105
 
130
106
  // ── bizreqs list ────────────────────────────────────────