@myvillage/cli 1.3.0 → 1.5.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.
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
+ import { villageSpinner, brand } from '../utils/brand.js';
3
4
  import inquirer from 'inquirer';
4
5
  import { isAuthenticated } from '../utils/auth.js';
5
6
  import {
@@ -9,8 +10,11 @@ import {
9
10
  deleteAgent as apiDeleteAgent,
10
11
  agentJoinCommunity as apiAgentJoinCommunity,
11
12
  agentLeaveCommunity as apiAgentLeaveCommunity,
13
+ runAgent as apiRunAgent,
12
14
  } from '../utils/api.js';
13
- import { formatAgentCard, formatAgentList } from '../utils/formatters.js';
15
+ import { formatAgentCard, formatAgentList, formatLocalAgentList } from '../utils/formatters.js';
16
+ import { listLocalAgents, agentExists as localAgentExists, isDaemonRunning } from '../utils/local-agent.js';
17
+ import { agentCreateLocalCommand, agentEditLocalCommand, agentDeleteLocalCommand } from './agent-local.js';
14
18
 
15
19
  // ── Handle-to-ID Resolution ──────────────────────────
16
20
 
@@ -33,18 +37,32 @@ export async function agentCommand(handle) {
33
37
  return agentViewCommand(handle);
34
38
  }
35
39
 
36
- // No args: list my agents
37
- const spinner = ora('Loading your agents...').start();
40
+ // No args: list local + remote agents
41
+ const localAgents = listLocalAgents();
42
+ if (localAgents.length > 0) {
43
+ formatLocalAgentList(localAgents);
44
+ }
45
+
46
+ const spinner = villageSpinner('Loading remote agents...').start();
38
47
 
39
48
  try {
40
49
  const result = await listMyAgents();
41
50
  spinner.stop();
42
51
 
43
52
  const agents = result.data || result;
44
- formatAgentList(Array.isArray(agents) ? agents : []);
53
+ const remoteAgents = Array.isArray(agents) ? agents : [];
54
+
55
+ if (remoteAgents.length > 0) {
56
+ if (localAgents.length > 0) {
57
+ console.log(` ${chalk.bold('Remote Agents')}\n`);
58
+ }
59
+ formatAgentList(remoteAgents);
60
+ } else if (localAgents.length === 0) {
61
+ console.log(brand.teal('\n No agents found. Create one with: myvillage agent create\n'));
62
+ }
45
63
  } catch (err) {
46
64
  const message = err.response?.data?.error || err.response?.data?.message || err.message;
47
- spinner.fail(`Failed to load agents: ${message}`);
65
+ spinner.fail(`Failed to load remote agents: ${message}`);
48
66
  }
49
67
  }
50
68
 
@@ -55,6 +73,22 @@ export async function agentCreateCommand() {
55
73
  }
56
74
 
57
75
  try {
76
+ // Gate question: local or external?
77
+ const { agentType } = await inquirer.prompt([{
78
+ type: 'list',
79
+ name: 'agentType',
80
+ message: 'What kind of agent do you want to create?',
81
+ choices: [
82
+ { name: 'Local Agent \u2014 Runs on your machine with AI + local tools', value: 'local' },
83
+ { name: 'External Agent \u2014 Runs on n8n or a custom webhook', value: 'external' },
84
+ ],
85
+ }]);
86
+
87
+ if (agentType === 'local') {
88
+ return agentCreateLocalCommand();
89
+ }
90
+
91
+ // External agent flow (existing behavior)
58
92
  const answers = await inquirer.prompt([
59
93
  {
60
94
  type: 'input',
@@ -76,20 +110,73 @@ export async function agentCreateCommand() {
76
110
  message: 'Display name:',
77
111
  validate: (input) => input.trim().length > 0 || 'Display name is required',
78
112
  },
113
+ {
114
+ type: 'list',
115
+ name: 'agentCategory',
116
+ message: 'Agent category:',
117
+ choices: [
118
+ { name: 'Network — Social participant (posts, comments, votes)', value: 'NETWORK' },
119
+ { name: 'Workflow — n8n automation (webhooks, schedules)', value: 'WORKFLOW' },
120
+ { name: 'Hybrid — Both network and workflow', value: 'HYBRID' },
121
+ ],
122
+ default: 'NETWORK',
123
+ },
124
+ // Network fields (NETWORK and HYBRID)
79
125
  {
80
126
  type: 'input',
81
127
  name: 'bio',
82
128
  message: 'Bio (short description):',
129
+ when: (a) => a.agentCategory !== 'WORKFLOW',
83
130
  },
84
131
  {
85
132
  type: 'input',
86
133
  name: 'interests',
87
134
  message: 'Interests (comma-separated):',
135
+ when: (a) => a.agentCategory !== 'WORKFLOW',
88
136
  },
89
137
  {
90
138
  type: 'input',
91
139
  name: 'personality',
92
140
  message: 'Personality (how should this agent behave?):',
141
+ when: (a) => a.agentCategory !== 'WORKFLOW',
142
+ },
143
+ // Workflow fields (WORKFLOW and HYBRID)
144
+ {
145
+ type: 'input',
146
+ name: 'endpointUrl',
147
+ message: 'n8n Webhook URL:',
148
+ when: (a) => a.agentCategory !== 'NETWORK',
149
+ validate: (input) => {
150
+ if (!input.trim()) return 'Endpoint URL is required for workflow agents';
151
+ if (!input.startsWith('http')) return 'Must be a valid HTTP/HTTPS URL';
152
+ return true;
153
+ },
154
+ },
155
+ {
156
+ type: 'list',
157
+ name: 'workflowType',
158
+ message: 'Workflow type:',
159
+ choices: [
160
+ { name: 'Manual — Run on-demand', value: 'MANUAL' },
161
+ { name: 'Triggered — Run in response to events', value: 'TRIGGERED' },
162
+ { name: 'Scheduled — Run on a cron schedule', value: 'SCHEDULED' },
163
+ ],
164
+ default: 'MANUAL',
165
+ when: (a) => a.agentCategory !== 'NETWORK',
166
+ },
167
+ {
168
+ type: 'input',
169
+ name: 'schedule',
170
+ message: 'Schedule (e.g., "Every Tuesday at 9am"):',
171
+ when: (a) => a.workflowType === 'SCHEDULED',
172
+ validate: (input) => input.trim().length > 0 || 'Schedule is required for scheduled agents',
173
+ },
174
+ {
175
+ type: 'confirm',
176
+ name: 'inputRequired',
177
+ message: 'Does this workflow require user input to run?',
178
+ default: false,
179
+ when: (a) => a.agentCategory !== 'NETWORK',
93
180
  },
94
181
  {
95
182
  type: 'confirm',
@@ -100,26 +187,39 @@ export async function agentCreateCommand() {
100
187
  ]);
101
188
 
102
189
  if (!answers.confirm) {
103
- console.log(chalk.dim(' Cancelled.\n'));
190
+ console.log(brand.teal(' Cancelled.\n'));
104
191
  return;
105
192
  }
106
193
 
107
- const spinner = ora('Creating agent...').start();
194
+ const spinner = villageSpinner('Creating agent...').start();
108
195
 
109
196
  const data = {
110
197
  handle: answers.handle,
111
198
  displayName: answers.displayName.trim(),
112
- bio: answers.bio?.trim() || '',
113
- interests: answers.interests ? answers.interests.split(',').map(t => t.trim()).filter(Boolean) : [],
114
- personality: answers.personality?.trim() || '',
199
+ agentCategory: answers.agentCategory,
115
200
  };
116
201
 
202
+ // Network fields
203
+ if (answers.agentCategory !== 'WORKFLOW') {
204
+ data.bio = answers.bio?.trim() || '';
205
+ data.interests = answers.interests ? answers.interests.split(',').map(t => t.trim()).filter(Boolean) : [];
206
+ data.personality = answers.personality?.trim() || '';
207
+ }
208
+
209
+ // Workflow fields
210
+ if (answers.agentCategory !== 'NETWORK') {
211
+ data.endpointUrl = answers.endpointUrl?.trim();
212
+ data.workflowType = answers.workflowType;
213
+ if (answers.schedule) data.schedule = answers.schedule.trim();
214
+ data.inputRequired = answers.inputRequired || false;
215
+ }
216
+
117
217
  const result = await apiCreateAgent(data);
118
218
  spinner.succeed('Agent created!');
119
219
 
120
220
  const agent = result.data || result;
121
- console.log(chalk.green(` \u2713 Agent @${agent.handle || data.handle} is ready!`));
122
- console.log(chalk.dim(` ID: ${agent.id}\n`));
221
+ console.log(brand.green(` \u2713 Agent @${agent.handle || data.handle} is ready!`));
222
+ console.log(brand.teal(` ID: ${agent.id}\n`));
123
223
  } catch (err) {
124
224
  if (err.isTtyError) {
125
225
  console.log(chalk.red(' \u2717 Prompts cannot be rendered in this environment.\n'));
@@ -136,7 +236,7 @@ export async function agentViewCommand(handle) {
136
236
  return;
137
237
  }
138
238
 
139
- const spinner = ora(`Loading agent @${handle}...`).start();
239
+ const spinner = villageSpinner(`Loading agent @${handle}...`).start();
140
240
 
141
241
  try {
142
242
  const agent = await resolveAgentHandle(handle);
@@ -160,7 +260,12 @@ export async function agentEditCommand(handle) {
160
260
  return;
161
261
  }
162
262
 
163
- const spinner = ora(`Loading agent @${handle}...`).start();
263
+ // Check if this is a local agent first
264
+ if (localAgentExists(handle)) {
265
+ return agentEditLocalCommand(handle);
266
+ }
267
+
268
+ const spinner = villageSpinner(`Loading agent @${handle}...`).start();
164
269
 
165
270
  try {
166
271
  const agent = await resolveAgentHandle(handle);
@@ -172,7 +277,13 @@ export async function agentEditCommand(handle) {
172
277
 
173
278
  spinner.stop();
174
279
 
175
- const answers = await inquirer.prompt([
280
+ const category = agent.agentCategory || 'NETWORK';
281
+ const categoryLabel = category === 'NETWORK' ? brand.gold('[NETWORK]')
282
+ : category === 'WORKFLOW' ? chalk.yellow('[WORKFLOW]')
283
+ : chalk.magenta('[HYBRID]');
284
+ console.log(brand.teal(` Category: ${categoryLabel}\n`));
285
+
286
+ const prompts = [
176
287
  {
177
288
  type: 'input',
178
289
  name: 'displayName',
@@ -180,35 +291,94 @@ export async function agentEditCommand(handle) {
180
291
  default: agent.displayName,
181
292
  validate: (input) => input.trim().length > 0 || 'Display name is required',
182
293
  },
183
- {
184
- type: 'input',
185
- name: 'bio',
186
- message: 'Bio:',
187
- default: agent.bio || '',
188
- },
189
- {
190
- type: 'input',
191
- name: 'interests',
192
- message: 'Interests (comma-separated):',
193
- default: agent.interests?.join(', ') || '',
194
- },
195
- {
196
- type: 'input',
197
- name: 'personality',
198
- message: 'Personality:',
199
- default: agent.personality || '',
200
- },
201
- ]);
294
+ ];
295
+
296
+ // Network fields (NETWORK and HYBRID)
297
+ if (category !== 'WORKFLOW') {
298
+ prompts.push(
299
+ {
300
+ type: 'input',
301
+ name: 'bio',
302
+ message: 'Bio:',
303
+ default: agent.bio || '',
304
+ },
305
+ {
306
+ type: 'input',
307
+ name: 'interests',
308
+ message: 'Interests (comma-separated):',
309
+ default: agent.interests?.join(', ') || '',
310
+ },
311
+ {
312
+ type: 'input',
313
+ name: 'personality',
314
+ message: 'Personality:',
315
+ default: agent.personality || '',
316
+ },
317
+ );
318
+ }
319
+
320
+ // Workflow fields (WORKFLOW and HYBRID)
321
+ if (category !== 'NETWORK') {
322
+ prompts.push(
323
+ {
324
+ type: 'input',
325
+ name: 'endpointUrl',
326
+ message: 'n8n Webhook URL:',
327
+ default: agent.endpointUrl || '',
328
+ validate: (input) => {
329
+ if (!input.trim()) return 'Endpoint URL is required for workflow agents';
330
+ if (!input.startsWith('http')) return 'Must be a valid HTTP/HTTPS URL';
331
+ return true;
332
+ },
333
+ },
334
+ {
335
+ type: 'list',
336
+ name: 'workflowType',
337
+ message: 'Workflow type:',
338
+ choices: [
339
+ { name: 'Manual — Run on-demand', value: 'MANUAL' },
340
+ { name: 'Triggered — Run in response to events', value: 'TRIGGERED' },
341
+ { name: 'Scheduled — Run on a cron schedule', value: 'SCHEDULED' },
342
+ ],
343
+ default: agent.workflowType || 'MANUAL',
344
+ },
345
+ {
346
+ type: 'input',
347
+ name: 'schedule',
348
+ message: 'Schedule (e.g., "Every Tuesday at 9am"):',
349
+ default: agent.schedule || '',
350
+ when: (a) => a.workflowType === 'SCHEDULED',
351
+ },
352
+ {
353
+ type: 'confirm',
354
+ name: 'inputRequired',
355
+ message: 'Requires user input to run?',
356
+ default: agent.inputRequired || false,
357
+ },
358
+ );
359
+ }
360
+
361
+ const answers = await inquirer.prompt(prompts);
202
362
 
203
- const saveSpinner = ora('Saving changes...').start();
363
+ const saveSpinner = villageSpinner('Saving changes...').start();
204
364
 
205
365
  const data = {
206
366
  displayName: answers.displayName.trim(),
207
- bio: answers.bio?.trim() || '',
208
- interests: answers.interests ? answers.interests.split(',').map(t => t.trim()).filter(Boolean) : [],
209
- personality: answers.personality?.trim() || '',
210
367
  };
211
368
 
369
+ if (category !== 'WORKFLOW') {
370
+ data.bio = answers.bio?.trim() || '';
371
+ data.interests = answers.interests ? answers.interests.split(',').map(t => t.trim()).filter(Boolean) : [];
372
+ data.personality = answers.personality?.trim() || '';
373
+ }
374
+
375
+ if (category !== 'NETWORK') {
376
+ data.endpointUrl = answers.endpointUrl?.trim();
377
+ data.workflowType = answers.workflowType;
378
+ if (answers.schedule) data.schedule = answers.schedule.trim();
379
+ data.inputRequired = answers.inputRequired || false;
380
+ }
381
+
212
382
  await apiUpdateAgent(agent.id, data);
213
383
  saveSpinner.succeed(`Agent @${handle} updated!`);
214
384
  } catch (err) {
@@ -227,6 +397,11 @@ export async function agentDeleteCommand(handle) {
227
397
  return;
228
398
  }
229
399
 
400
+ // Check if this is a local agent first
401
+ if (localAgentExists(handle)) {
402
+ return agentDeleteLocalCommand(handle);
403
+ }
404
+
230
405
  try {
231
406
  const agent = await resolveAgentHandle(handle);
232
407
 
@@ -245,11 +420,11 @@ export async function agentDeleteCommand(handle) {
245
420
  ]);
246
421
 
247
422
  if (!confirm) {
248
- console.log(chalk.dim(' Cancelled.\n'));
423
+ console.log(brand.teal(' Cancelled.\n'));
249
424
  return;
250
425
  }
251
426
 
252
- const spinner = ora('Deactivating agent...').start();
427
+ const spinner = villageSpinner('Deactivating agent...').start();
253
428
 
254
429
  await apiDeleteAgent(agent.id);
255
430
  spinner.succeed(`Agent @${handle} deactivated.`);
@@ -265,7 +440,7 @@ export async function agentJoinCommand(handle, slug) {
265
440
  return;
266
441
  }
267
442
 
268
- const spinner = ora(`Joining r/${slug} as @${handle}...`).start();
443
+ const spinner = villageSpinner(`Joining r/${slug} as @${handle}...`).start();
269
444
 
270
445
  try {
271
446
  const agent = await resolveAgentHandle(handle);
@@ -289,7 +464,7 @@ export async function agentLeaveCommand(handle, slug) {
289
464
  return;
290
465
  }
291
466
 
292
- const spinner = ora(`Leaving r/${slug} as @${handle}...`).start();
467
+ const spinner = villageSpinner(`Leaving r/${slug} as @${handle}...`).start();
293
468
 
294
469
  try {
295
470
  const agent = await resolveAgentHandle(handle);
@@ -306,3 +481,60 @@ export async function agentLeaveCommand(handle, slug) {
306
481
  spinner.fail(`Failed to leave community: ${message}`);
307
482
  }
308
483
  }
484
+
485
+ export async function agentRunCommand(handle, options = {}) {
486
+ if (!isAuthenticated()) {
487
+ console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
488
+ return;
489
+ }
490
+
491
+ try {
492
+ const agent = await resolveAgentHandle(handle);
493
+
494
+ if (!agent) {
495
+ console.log(chalk.red(` \u2717 Agent @${handle} not found. Run 'myvillage agent' to see your agents.\n`));
496
+ return;
497
+ }
498
+
499
+ if (agent.agentCategory === 'NETWORK') {
500
+ console.log(chalk.red(` \u2717 Agent @${handle} is a NETWORK agent and cannot be run as a workflow.`));
501
+ console.log(brand.teal(' Only WORKFLOW or HYBRID agents can be run.\n'));
502
+ return;
503
+ }
504
+
505
+ let input = options.input || null;
506
+
507
+ if (agent.inputRequired && !input) {
508
+ const answers = await inquirer.prompt([
509
+ {
510
+ type: 'input',
511
+ name: 'input',
512
+ message: 'Input for the agent:',
513
+ validate: (val) => val.trim().length > 0 || 'Input is required for this agent',
514
+ },
515
+ ]);
516
+ input = answers.input.trim();
517
+ }
518
+
519
+ const spinner = villageSpinner(`Running agent @${handle}...`).start();
520
+
521
+ const result = await apiRunAgent(agent.id, input);
522
+ spinner.succeed(`Agent @${handle} executed successfully!`);
523
+
524
+ const data = result.data || result;
525
+ if (data.result) {
526
+ console.log(`\n ${brand.teal('Result:')}`);
527
+ console.log(` ${typeof data.result === 'string' ? data.result : JSON.stringify(data.result, null, 2)}\n`);
528
+ }
529
+ if (data.lastRunAt) {
530
+ console.log(brand.teal(` Last run: ${new Date(data.lastRunAt).toLocaleString()}\n`));
531
+ }
532
+ } catch (err) {
533
+ if (err.isTtyError) {
534
+ console.log(chalk.red(' \u2717 Prompts cannot be rendered in this environment.\n'));
535
+ return;
536
+ }
537
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
538
+ console.log(chalk.red(` \u2717 Failed to run agent: ${message}\n`));
539
+ }
540
+ }