@myvillage/cli 1.10.2 → 1.18.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/loop.js +215 -6
- package/src/commands/agent-client.js +435 -0
- package/src/commands/agent-grant.js +131 -0
- package/src/commands/agent-local.js +395 -1
- package/src/commands/create-app.js +61 -1
- package/src/commands/media.js +185 -187
- package/src/commands/wisdom.js +185 -0
- package/src/index.js +212 -0
- package/src/utils/agent-scaffolder.js +8 -0
- package/src/utils/agentic-templates.js +10 -2
- package/src/utils/api.js +179 -0
- package/src/utils/formatters.js +72 -0
- package/src/utils/wisdom.js +102 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync } from 'fs';
|
|
4
|
+
import { villageSpinner, brand } from '../utils/brand.js';
|
|
5
|
+
import { isAuthenticated } from '../utils/auth.js';
|
|
6
|
+
import {
|
|
7
|
+
listMyAgents,
|
|
8
|
+
registerClientAgent as apiRegisterClientAgent,
|
|
9
|
+
listClientAgentConfigs as apiListClientAgentConfigs,
|
|
10
|
+
getClientAgentConfig as apiGetClientAgentConfig,
|
|
11
|
+
updateClientAgentConfig as apiUpdateClientAgentConfig,
|
|
12
|
+
deactivateClientAgent as apiDeactivateClientAgent,
|
|
13
|
+
rotateClientAgentKey as apiRotateClientAgentKey,
|
|
14
|
+
} from '../utils/api.js';
|
|
15
|
+
import { formatClientAgentConfig, formatClientAgentList } from '../utils/formatters.js';
|
|
16
|
+
|
|
17
|
+
// ── Schedule Presets ────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const SCHEDULE_PRESETS = [
|
|
20
|
+
{ name: 'Daily at 9am', value: '0 9 * * *' },
|
|
21
|
+
{ name: 'Weekly Monday 9am', value: '0 9 * * 1' },
|
|
22
|
+
{ name: 'Monthly 1st at 9am', value: '0 9 1 * *' },
|
|
23
|
+
{ name: 'Custom cron expression', value: 'CUSTOM' },
|
|
24
|
+
{ name: 'None (manual only)', value: '' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const WORKFLOW_TYPES = [
|
|
28
|
+
{ name: 'Submission Processor — classify submissions, draft outreach', value: 'SUBMISSION_PROCESSOR' },
|
|
29
|
+
{ name: 'Digest Generator — weekly/daily activity summaries', value: 'DIGEST_GENERATOR' },
|
|
30
|
+
{ name: 'Member Matcher — match new signups with existing members', value: 'MEMBER_MATCHER' },
|
|
31
|
+
{ name: 'Stale Data Detector — flag inactive members', value: 'STALE_DATA_DETECTOR' },
|
|
32
|
+
{ name: 'Custom', value: 'CUSTOM' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// ── Helpers ─────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
function displayApiKey(apiKey, clientId) {
|
|
38
|
+
console.log('');
|
|
39
|
+
console.log(chalk.yellow.bold(' ⚠ IMPORTANT: Save your API key now — it will not be shown again!'));
|
|
40
|
+
console.log('');
|
|
41
|
+
console.log(` ${chalk.dim('Client ID:')} ${brand.teal(clientId)}`);
|
|
42
|
+
console.log(` ${chalk.dim('API Key:')} ${brand.gold(apiKey)}`);
|
|
43
|
+
console.log('');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function promptSaveToEnvFile(apiKey, clientId) {
|
|
47
|
+
const { saveToFile } = await inquirer.prompt([{
|
|
48
|
+
type: 'confirm',
|
|
49
|
+
name: 'saveToFile',
|
|
50
|
+
message: 'Save API key to a .env file?',
|
|
51
|
+
default: false,
|
|
52
|
+
}]);
|
|
53
|
+
|
|
54
|
+
if (saveToFile) {
|
|
55
|
+
const { filePath } = await inquirer.prompt([{
|
|
56
|
+
type: 'input',
|
|
57
|
+
name: 'filePath',
|
|
58
|
+
message: 'File path:',
|
|
59
|
+
default: '.env.local',
|
|
60
|
+
}]);
|
|
61
|
+
|
|
62
|
+
const lines = `\n# MyVillageOS Client Agent\nMYVILLAGE_AGENT_API_KEY=${apiKey}\nMYVILLAGE_AGENT_CLIENT_ID=${clientId}\n`;
|
|
63
|
+
|
|
64
|
+
if (existsSync(filePath)) {
|
|
65
|
+
appendFileSync(filePath, lines);
|
|
66
|
+
} else {
|
|
67
|
+
writeFileSync(filePath, lines.trimStart());
|
|
68
|
+
}
|
|
69
|
+
console.log(brand.green(` ✓ API key saved to ${filePath}\n`));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function resolveAgentId(handle) {
|
|
74
|
+
const result = await listMyAgents();
|
|
75
|
+
const agents = result.data || result;
|
|
76
|
+
if (!Array.isArray(agents) || agents.length === 0) return null;
|
|
77
|
+
|
|
78
|
+
const match = agents.find(a =>
|
|
79
|
+
a.handle === handle || a.handle === handle.replace(/^@/, '')
|
|
80
|
+
);
|
|
81
|
+
return match || null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Register Client ─────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
export async function agentRegisterClientCommand(options) {
|
|
87
|
+
if (!isAuthenticated()) {
|
|
88
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.\n'));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const isNonInteractive = options.agent && options.clientId && options.name && options.url;
|
|
94
|
+
|
|
95
|
+
let data;
|
|
96
|
+
|
|
97
|
+
if (isNonInteractive) {
|
|
98
|
+
// Resolve agent handle to ID
|
|
99
|
+
const spinner = villageSpinner('Resolving agent...').start();
|
|
100
|
+
const agent = await resolveAgentId(options.agent);
|
|
101
|
+
if (!agent) {
|
|
102
|
+
spinner.fail(`Agent "${options.agent}" not found.`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
spinner.succeed(`Found agent @${agent.handle}`);
|
|
106
|
+
|
|
107
|
+
data = {
|
|
108
|
+
villageAgentId: agent.villageAgent?.id || agent.villageAgentId || agent.id,
|
|
109
|
+
clientId: options.clientId.toLowerCase(),
|
|
110
|
+
clientName: options.name,
|
|
111
|
+
baseUrl: options.url,
|
|
112
|
+
workflowType: (options.workflow || 'SUBMISSION_PROCESSOR').toUpperCase(),
|
|
113
|
+
schedule: options.schedule || null,
|
|
114
|
+
timezone: options.timezone || 'America/Chicago',
|
|
115
|
+
};
|
|
116
|
+
} else {
|
|
117
|
+
// Interactive mode
|
|
118
|
+
const agentSpinner = villageSpinner('Loading your agents...').start();
|
|
119
|
+
const agentResult = await listMyAgents();
|
|
120
|
+
const agents = agentResult.data || agentResult;
|
|
121
|
+
agentSpinner.stop();
|
|
122
|
+
|
|
123
|
+
if (!Array.isArray(agents) || agents.length === 0) {
|
|
124
|
+
console.log(brand.teal('\n No agents found. Create one first with: myvillage agent create\n'));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const answers = await inquirer.prompt([
|
|
129
|
+
{
|
|
130
|
+
type: 'list',
|
|
131
|
+
name: 'selectedAgent',
|
|
132
|
+
message: 'Which agent should manage this client?',
|
|
133
|
+
choices: agents.map(a => ({
|
|
134
|
+
name: `@${a.handle} — ${a.displayName || a.handle}`,
|
|
135
|
+
value: a,
|
|
136
|
+
})),
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
type: 'input',
|
|
140
|
+
name: 'clientId',
|
|
141
|
+
message: 'Client identifier (lowercase, no spaces):',
|
|
142
|
+
validate: input => /^[a-z0-9_-]{3,40}$/.test(input) || 'Must be 3-40 chars: lowercase letters, numbers, hyphens, underscores.',
|
|
143
|
+
filter: input => input.toLowerCase().replace(/[^a-z0-9_-]/g, ''),
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
type: 'input',
|
|
147
|
+
name: 'clientName',
|
|
148
|
+
message: 'Client display name:',
|
|
149
|
+
validate: input => input.trim().length > 0 || 'Name is required.',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: 'input',
|
|
153
|
+
name: 'baseUrl',
|
|
154
|
+
message: 'Client base URL:',
|
|
155
|
+
validate: input => {
|
|
156
|
+
try {
|
|
157
|
+
new URL(input);
|
|
158
|
+
return true;
|
|
159
|
+
} catch {
|
|
160
|
+
return 'Must be a valid URL (e.g., https://maywood-chatkit.vercel.app)';
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
type: 'list',
|
|
166
|
+
name: 'workflowType',
|
|
167
|
+
message: 'Workflow type:',
|
|
168
|
+
choices: WORKFLOW_TYPES,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
type: 'list',
|
|
172
|
+
name: 'schedulePreset',
|
|
173
|
+
message: 'Schedule:',
|
|
174
|
+
choices: SCHEDULE_PRESETS,
|
|
175
|
+
},
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
let schedule = answers.schedulePreset;
|
|
179
|
+
if (schedule === 'CUSTOM') {
|
|
180
|
+
const { customCron } = await inquirer.prompt([{
|
|
181
|
+
type: 'input',
|
|
182
|
+
name: 'customCron',
|
|
183
|
+
message: 'Cron expression (e.g., 0 9 * * 1):',
|
|
184
|
+
validate: input => input.trim().split(/\s+/).length === 5 || 'Must be a 5-field cron expression.',
|
|
185
|
+
}]);
|
|
186
|
+
schedule = customCron;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const { confirm } = await inquirer.prompt([{
|
|
190
|
+
type: 'confirm',
|
|
191
|
+
name: 'confirm',
|
|
192
|
+
message: `Register "${answers.clientName}" (${answers.clientId}) with @${answers.selectedAgent.handle}?`,
|
|
193
|
+
default: true,
|
|
194
|
+
}]);
|
|
195
|
+
|
|
196
|
+
if (!confirm) {
|
|
197
|
+
console.log(chalk.dim('\n Cancelled.\n'));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const selectedAgent = answers.selectedAgent;
|
|
202
|
+
data = {
|
|
203
|
+
villageAgentId: selectedAgent.villageAgent?.id || selectedAgent.villageAgentId || selectedAgent.id,
|
|
204
|
+
clientId: answers.clientId,
|
|
205
|
+
clientName: answers.clientName,
|
|
206
|
+
baseUrl: answers.baseUrl,
|
|
207
|
+
workflowType: answers.workflowType,
|
|
208
|
+
schedule: schedule || null,
|
|
209
|
+
timezone: options.timezone || 'America/Chicago',
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const regSpinner = villageSpinner('Registering client agent...').start();
|
|
214
|
+
const result = await apiRegisterClientAgent(data);
|
|
215
|
+
regSpinner.succeed('Client agent registered!');
|
|
216
|
+
|
|
217
|
+
displayApiKey(result.apiKey, data.clientId);
|
|
218
|
+
|
|
219
|
+
if (!isNonInteractive) {
|
|
220
|
+
await promptSaveToEnvFile(result.apiKey, data.clientId);
|
|
221
|
+
} else if (options.envFile) {
|
|
222
|
+
const lines = `\n# MyVillageOS Client Agent\nMYVILLAGE_AGENT_API_KEY=${result.apiKey}\nMYVILLAGE_AGENT_CLIENT_ID=${data.clientId}\n`;
|
|
223
|
+
if (existsSync(options.envFile)) {
|
|
224
|
+
appendFileSync(options.envFile, lines);
|
|
225
|
+
} else {
|
|
226
|
+
writeFileSync(options.envFile, lines.trimStart());
|
|
227
|
+
}
|
|
228
|
+
console.log(brand.green(` ✓ API key saved to ${options.envFile}\n`));
|
|
229
|
+
}
|
|
230
|
+
} catch (err) {
|
|
231
|
+
const message = err.response?.data?.error || err.response?.data?.message || err.message;
|
|
232
|
+
console.log(chalk.red(`\n ✗ Failed to register client agent: ${message}\n`));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── List Clients ────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
export async function agentListClientsCommand() {
|
|
239
|
+
if (!isAuthenticated()) {
|
|
240
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.\n'));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const spinner = villageSpinner('Loading client agent configs...').start();
|
|
246
|
+
const result = await apiListClientAgentConfigs();
|
|
247
|
+
spinner.stop();
|
|
248
|
+
|
|
249
|
+
const configs = result.data || result;
|
|
250
|
+
formatClientAgentList(Array.isArray(configs) ? configs : []);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
const message = err.response?.data?.error || err.response?.data?.message || err.message;
|
|
253
|
+
console.log(chalk.red(`\n ✗ Failed to list client agents: ${message}\n`));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── View Client ─────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
export async function agentViewClientCommand(configId) {
|
|
260
|
+
if (!isAuthenticated()) {
|
|
261
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.\n'));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const spinner = villageSpinner('Loading client agent config...').start();
|
|
267
|
+
const result = await apiGetClientAgentConfig(configId);
|
|
268
|
+
spinner.stop();
|
|
269
|
+
|
|
270
|
+
const config = result.data || result;
|
|
271
|
+
formatClientAgentConfig(config);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
const message = err.response?.data?.error || err.response?.data?.message || err.message;
|
|
274
|
+
console.log(chalk.red(`\n ✗ Failed to load client agent config: ${message}\n`));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── Edit Client ─────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
export async function agentEditClientCommand(configId) {
|
|
281
|
+
if (!isAuthenticated()) {
|
|
282
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.\n'));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const spinner = villageSpinner('Loading client agent config...').start();
|
|
288
|
+
const result = await apiGetClientAgentConfig(configId);
|
|
289
|
+
spinner.stop();
|
|
290
|
+
|
|
291
|
+
const config = result.data || result;
|
|
292
|
+
|
|
293
|
+
console.log(chalk.dim(`\n Editing: ${config.clientName} (${config.clientId})\n`));
|
|
294
|
+
|
|
295
|
+
// Match current schedule to a preset
|
|
296
|
+
const currentPresetMatch = SCHEDULE_PRESETS.find(p => p.value === config.schedule);
|
|
297
|
+
const currentPreset = currentPresetMatch ? currentPresetMatch.value : (config.schedule ? 'CUSTOM' : '');
|
|
298
|
+
|
|
299
|
+
const answers = await inquirer.prompt([
|
|
300
|
+
{
|
|
301
|
+
type: 'input',
|
|
302
|
+
name: 'clientName',
|
|
303
|
+
message: 'Client name:',
|
|
304
|
+
default: config.clientName,
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
type: 'input',
|
|
308
|
+
name: 'baseUrl',
|
|
309
|
+
message: 'Base URL:',
|
|
310
|
+
default: config.baseUrl,
|
|
311
|
+
validate: input => {
|
|
312
|
+
try { new URL(input); return true; } catch { return 'Must be a valid URL.'; }
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
type: 'list',
|
|
317
|
+
name: 'workflowType',
|
|
318
|
+
message: 'Workflow type:',
|
|
319
|
+
choices: WORKFLOW_TYPES,
|
|
320
|
+
default: config.workflowType,
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
type: 'list',
|
|
324
|
+
name: 'schedulePreset',
|
|
325
|
+
message: 'Schedule:',
|
|
326
|
+
choices: SCHEDULE_PRESETS,
|
|
327
|
+
default: currentPreset,
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
type: 'input',
|
|
331
|
+
name: 'timezone',
|
|
332
|
+
message: 'Timezone:',
|
|
333
|
+
default: config.timezone || 'America/Chicago',
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
type: 'confirm',
|
|
337
|
+
name: 'isActive',
|
|
338
|
+
message: 'Active?',
|
|
339
|
+
default: config.isActive,
|
|
340
|
+
},
|
|
341
|
+
]);
|
|
342
|
+
|
|
343
|
+
let schedule = answers.schedulePreset;
|
|
344
|
+
if (schedule === 'CUSTOM') {
|
|
345
|
+
const { customCron } = await inquirer.prompt([{
|
|
346
|
+
type: 'input',
|
|
347
|
+
name: 'customCron',
|
|
348
|
+
message: 'Cron expression:',
|
|
349
|
+
default: config.schedule || '',
|
|
350
|
+
validate: input => input.trim().split(/\s+/).length === 5 || 'Must be a 5-field cron expression.',
|
|
351
|
+
}]);
|
|
352
|
+
schedule = customCron;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const updateData = {
|
|
356
|
+
clientName: answers.clientName,
|
|
357
|
+
baseUrl: answers.baseUrl,
|
|
358
|
+
workflowType: answers.workflowType,
|
|
359
|
+
schedule: schedule || null,
|
|
360
|
+
timezone: answers.timezone,
|
|
361
|
+
isActive: answers.isActive,
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const saveSpinner = villageSpinner('Saving changes...').start();
|
|
365
|
+
await apiUpdateClientAgentConfig(configId, updateData);
|
|
366
|
+
saveSpinner.succeed('Client agent config updated!');
|
|
367
|
+
console.log('');
|
|
368
|
+
} catch (err) {
|
|
369
|
+
const message = err.response?.data?.error || err.response?.data?.message || err.message;
|
|
370
|
+
console.log(chalk.red(`\n ✗ Failed to update client agent config: ${message}\n`));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ── Deactivate Client ───────────────────────────────────
|
|
375
|
+
|
|
376
|
+
export async function agentDeactivateClientCommand(configId) {
|
|
377
|
+
if (!isAuthenticated()) {
|
|
378
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.\n'));
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const { confirm } = await inquirer.prompt([{
|
|
384
|
+
type: 'confirm',
|
|
385
|
+
name: 'confirm',
|
|
386
|
+
message: `Deactivate client agent config ${configId}? This will stop all automated workflows.`,
|
|
387
|
+
default: false,
|
|
388
|
+
}]);
|
|
389
|
+
|
|
390
|
+
if (!confirm) {
|
|
391
|
+
console.log(chalk.dim('\n Cancelled.\n'));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const spinner = villageSpinner('Deactivating...').start();
|
|
396
|
+
await apiDeactivateClientAgent(configId);
|
|
397
|
+
spinner.succeed('Client agent deactivated.');
|
|
398
|
+
console.log('');
|
|
399
|
+
} catch (err) {
|
|
400
|
+
const message = err.response?.data?.error || err.response?.data?.message || err.message;
|
|
401
|
+
console.log(chalk.red(`\n ✗ Failed to deactivate: ${message}\n`));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ── Rotate Client Key ───────────────────────────────────
|
|
406
|
+
|
|
407
|
+
export async function agentRotateClientKeyCommand(configId) {
|
|
408
|
+
if (!isAuthenticated()) {
|
|
409
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.\n'));
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
const { confirm } = await inquirer.prompt([{
|
|
415
|
+
type: 'confirm',
|
|
416
|
+
name: 'confirm',
|
|
417
|
+
message: `Rotate API key for ${configId}? The current key will be invalidated immediately.`,
|
|
418
|
+
default: false,
|
|
419
|
+
}]);
|
|
420
|
+
|
|
421
|
+
if (!confirm) {
|
|
422
|
+
console.log(chalk.dim('\n Cancelled.\n'));
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const spinner = villageSpinner('Rotating API key...').start();
|
|
427
|
+
const result = await apiRotateClientAgentKey(configId);
|
|
428
|
+
spinner.succeed('API key rotated!');
|
|
429
|
+
|
|
430
|
+
displayApiKey(result.apiKey, configId);
|
|
431
|
+
} catch (err) {
|
|
432
|
+
const message = err.response?.data?.error || err.response?.data?.message || err.message;
|
|
433
|
+
console.log(chalk.red(`\n ✗ Failed to rotate API key: ${message}\n`));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { isAuthenticated } from '../utils/auth.js';
|
|
3
|
+
import { readAgentConfig } from '../utils/local-agent.js';
|
|
4
|
+
import { getPlatformClient } from '../utils/api.js';
|
|
5
|
+
|
|
6
|
+
const VALID_PROVIDERS = ['google', 'microsoft', 'zoom'];
|
|
7
|
+
|
|
8
|
+
function ensureAuthed() {
|
|
9
|
+
if (!isAuthenticated()) {
|
|
10
|
+
console.log(chalk.red(" ✗ Authentication required. Run 'myvillage login' first."));
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolveAgentId(name) {
|
|
17
|
+
const config = readAgentConfig(name);
|
|
18
|
+
if (!config) {
|
|
19
|
+
console.log(chalk.red(` ✗ Local agent "${name}" not found`));
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const agentId = config?.man?.agent_id;
|
|
23
|
+
if (!agentId) {
|
|
24
|
+
console.log(
|
|
25
|
+
chalk.red(
|
|
26
|
+
` ✗ Agent "${name}" has not been registered on the platform yet. ` +
|
|
27
|
+
`Run 'myvillage agent start ${name}' once to register it.`,
|
|
28
|
+
),
|
|
29
|
+
);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return agentId;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeProvider(provider) {
|
|
36
|
+
const lc = (provider || '').toLowerCase();
|
|
37
|
+
if (!VALID_PROVIDERS.includes(lc)) {
|
|
38
|
+
console.log(
|
|
39
|
+
chalk.red(` ✗ Invalid provider "${provider}". Must be one of: ${VALID_PROVIDERS.join(', ')}`),
|
|
40
|
+
);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return lc.toUpperCase();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function printApiError(err, fallback) {
|
|
47
|
+
const status = err?.response?.status;
|
|
48
|
+
const data = err?.response?.data;
|
|
49
|
+
const code = data?.code ? ` (${data.code})` : '';
|
|
50
|
+
const message = data?.error || err?.message || fallback;
|
|
51
|
+
console.log(chalk.red(` ✗ ${message}${code}${status ? ` [HTTP ${status}]` : ''}`));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── List grants ────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
export async function agentGrantsCommand(name) {
|
|
57
|
+
if (!ensureAuthed()) return;
|
|
58
|
+
const agentId = resolveAgentId(name);
|
|
59
|
+
if (!agentId) return;
|
|
60
|
+
|
|
61
|
+
const client = getPlatformClient();
|
|
62
|
+
try {
|
|
63
|
+
const response = await client.get(`/agents/${agentId}/credential-grants`);
|
|
64
|
+
const grants = response.data?.data ?? [];
|
|
65
|
+
if (grants.length === 0) {
|
|
66
|
+
console.log(chalk.dim(` No active credential grants for agent "${name}".`));
|
|
67
|
+
console.log(chalk.dim(` Grant one with: myvillage agent grant ${name} <google|microsoft|zoom>`));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
console.log(chalk.bold(`Active credential grants for "${name}" (${agentId}):\n`));
|
|
71
|
+
for (const g of grants) {
|
|
72
|
+
const granted = new Date(g.grantedAt).toISOString().slice(0, 10);
|
|
73
|
+
console.log(` - ${chalk.cyan(g.provider)} granted ${granted}`);
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
printApiError(err, 'Failed to list grants');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Grant a provider ───────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
export async function agentGrantCommand(name, provider) {
|
|
83
|
+
if (!ensureAuthed()) return;
|
|
84
|
+
const agentId = resolveAgentId(name);
|
|
85
|
+
if (!agentId) return;
|
|
86
|
+
const upperProvider = normalizeProvider(provider);
|
|
87
|
+
if (!upperProvider) return;
|
|
88
|
+
|
|
89
|
+
const client = getPlatformClient();
|
|
90
|
+
try {
|
|
91
|
+
await client.post(`/agents/${agentId}/credential-grants`, { provider: upperProvider });
|
|
92
|
+
console.log(
|
|
93
|
+
chalk.green(` ✓ Granted ${upperProvider} to agent "${name}".`) +
|
|
94
|
+
chalk.dim(`\n The agent can now use ${upperProvider}-backed tools (e.g. gmail_send).`),
|
|
95
|
+
);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
if (err?.response?.status === 404 && err?.response?.data?.code === 'CREDENTIAL_NOT_CONNECTED') {
|
|
98
|
+
console.log(
|
|
99
|
+
chalk.yellow(
|
|
100
|
+
` ⚠ You haven't connected ${upperProvider} yet. ` +
|
|
101
|
+
`Connect it from the mobile app or web portal, then re-run this command.`,
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
printApiError(err, 'Failed to grant provider');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Revoke a provider ──────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
export async function agentRevokeCommand(name, provider) {
|
|
113
|
+
if (!ensureAuthed()) return;
|
|
114
|
+
const agentId = resolveAgentId(name);
|
|
115
|
+
if (!agentId) return;
|
|
116
|
+
const upperProvider = normalizeProvider(provider);
|
|
117
|
+
if (!upperProvider) return;
|
|
118
|
+
|
|
119
|
+
const client = getPlatformClient();
|
|
120
|
+
try {
|
|
121
|
+
await client.delete(`/agents/${agentId}/credential-grants/${upperProvider}`);
|
|
122
|
+
console.log(
|
|
123
|
+
chalk.green(` ✓ Revoked ${upperProvider} from agent "${name}".`) +
|
|
124
|
+
chalk.dim(
|
|
125
|
+
`\n Tools using ${upperProvider} will fail within ~60 seconds (cache TTL).`,
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
printApiError(err, 'Failed to revoke provider');
|
|
130
|
+
}
|
|
131
|
+
}
|