@myvillage/cli 1.74.0 → 1.75.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/commands/agent-credentials.js +158 -0
- package/src/index.js +23 -0
- package/src/utils/api.js +28 -0
package/package.json
CHANGED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { villageSpinner, brand } from '../utils/brand.js';
|
|
3
|
+
import { isAuthenticated } from '../utils/auth.js';
|
|
4
|
+
import { agentExists, readAgentConfig } from '../utils/local-agent.js';
|
|
5
|
+
import {
|
|
6
|
+
listAgentCredentialGrants,
|
|
7
|
+
grantAgentCredential,
|
|
8
|
+
revokeAgentCredential,
|
|
9
|
+
} from '../utils/api.js';
|
|
10
|
+
|
|
11
|
+
// ── Agent OAuth credential grants ───────────────────────
|
|
12
|
+
//
|
|
13
|
+
// `myvillage agent grant/revoke/grants <name> [provider]` — authorize a local
|
|
14
|
+
// agent to use the villager's connected providers (Google, Microsoft, Zoom)
|
|
15
|
+
// when it runs through the MyVillage MCP server. The platform vends
|
|
16
|
+
// short-lived tokens to the MCP server; raw OAuth tokens never touch the CLI.
|
|
17
|
+
//
|
|
18
|
+
// The villager connects the provider once in MyVillage Studio or at
|
|
19
|
+
// myvillageproject.ai. These commands only manage the per-agent consent.
|
|
20
|
+
|
|
21
|
+
const VALID_PROVIDERS = ['google', 'microsoft', 'zoom'];
|
|
22
|
+
|
|
23
|
+
// Human-facing labels keyed by the lowercase slug the CLI accepts.
|
|
24
|
+
const PROVIDER_LABELS = {
|
|
25
|
+
google: 'Google (Gmail, Calendar, Drive)',
|
|
26
|
+
microsoft: 'Microsoft 365 (Outlook mail, Calendar)',
|
|
27
|
+
zoom: 'Zoom',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function fail(message) {
|
|
31
|
+
console.log(chalk.red(` ✗ ${message}`));
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeProvider(provider) {
|
|
36
|
+
return (provider || '').trim().toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Resolve the AgentProfile id (man.agent_id) for a local agent, with friendly
|
|
40
|
+
// errors when the agent is unknown or hasn't been registered yet.
|
|
41
|
+
function resolveAgentId(name) {
|
|
42
|
+
if (!agentExists(name)) {
|
|
43
|
+
return { error: `Agent "${name}" not found. Run \`myvillage agent\` to see your agents.` };
|
|
44
|
+
}
|
|
45
|
+
const config = readAgentConfig(name);
|
|
46
|
+
const agentId = config?.man?.agent_id;
|
|
47
|
+
if (!agentId) {
|
|
48
|
+
return {
|
|
49
|
+
error:
|
|
50
|
+
`Agent "${name}" hasn't been registered with the network yet. ` +
|
|
51
|
+
`Run \`myvillage agent start ${name}\` once so it gets an agent id, then retry.`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return { agentId };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Map a platform error response to a villager-readable, actionable message.
|
|
58
|
+
function describeGrantError(err, provider) {
|
|
59
|
+
const data = err.response?.data;
|
|
60
|
+
const code = data?.code;
|
|
61
|
+
const label = PROVIDER_LABELS[provider] || provider;
|
|
62
|
+
switch (code) {
|
|
63
|
+
case 'CREDENTIAL_NOT_CONNECTED':
|
|
64
|
+
return `${label} isn't connected for your account. Connect it in MyVillage Studio or at myvillageproject.ai, then retry.`;
|
|
65
|
+
case 'GRANT_NOT_FOUND':
|
|
66
|
+
return `No active ${label} grant for this agent.`;
|
|
67
|
+
case 'FORBIDDEN_NOT_OWNER':
|
|
68
|
+
return `You don't own this agent, so you can't change its credential grants.`;
|
|
69
|
+
case 'AGENT_NOT_FOUND':
|
|
70
|
+
return `The platform has no record of this agent. Run \`myvillage agent start\` to register it.`;
|
|
71
|
+
default:
|
|
72
|
+
return data?.error || err.message;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── grant ───────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
export async function agentGrantCommand(name, provider) {
|
|
79
|
+
if (!isAuthenticated()) return fail('Authentication required. Run `myvillage login` first.');
|
|
80
|
+
|
|
81
|
+
const slug = normalizeProvider(provider);
|
|
82
|
+
if (!VALID_PROVIDERS.includes(slug)) {
|
|
83
|
+
return fail(`Invalid provider "${provider}". Must be one of: ${VALID_PROVIDERS.join(', ')}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const { agentId, error } = resolveAgentId(name);
|
|
87
|
+
if (error) return fail(error);
|
|
88
|
+
|
|
89
|
+
const label = PROVIDER_LABELS[slug];
|
|
90
|
+
const spinner = villageSpinner(`Granting ${label} to "${name}"...`).start();
|
|
91
|
+
try {
|
|
92
|
+
await grantAgentCredential(agentId, slug.toUpperCase());
|
|
93
|
+
spinner.succeed(`"${name}" can now use your ${label}.`);
|
|
94
|
+
console.log(brand.teal(`\n The agent reads credentials through the MyVillage MCP server — no tokens are stored locally.`));
|
|
95
|
+
console.log(brand.teal(` Revoke any time: ${chalk.bold(`myvillage agent revoke ${name} ${slug}`)}\n`));
|
|
96
|
+
} catch (err) {
|
|
97
|
+
spinner.fail(`Failed to grant ${label}: ${describeGrantError(err, slug)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── revoke ──────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
export async function agentRevokeCredentialCommand(name, provider) {
|
|
104
|
+
if (!isAuthenticated()) return fail('Authentication required. Run `myvillage login` first.');
|
|
105
|
+
|
|
106
|
+
const slug = normalizeProvider(provider);
|
|
107
|
+
if (!VALID_PROVIDERS.includes(slug)) {
|
|
108
|
+
return fail(`Invalid provider "${provider}". Must be one of: ${VALID_PROVIDERS.join(', ')}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { agentId, error } = resolveAgentId(name);
|
|
112
|
+
if (error) return fail(error);
|
|
113
|
+
|
|
114
|
+
const label = PROVIDER_LABELS[slug];
|
|
115
|
+
const spinner = villageSpinner(`Revoking ${label} from "${name}"...`).start();
|
|
116
|
+
try {
|
|
117
|
+
await revokeAgentCredential(agentId, slug.toUpperCase());
|
|
118
|
+
spinner.succeed(`Revoked ${label} from "${name}". (Propagates to running agents within ~60s.)`);
|
|
119
|
+
console.log('');
|
|
120
|
+
} catch (err) {
|
|
121
|
+
spinner.fail(`Failed to revoke ${label}: ${describeGrantError(err, slug)}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── grants (list) ───────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
export async function agentGrantsListCommand(name) {
|
|
128
|
+
if (!isAuthenticated()) return fail('Authentication required. Run `myvillage login` first.');
|
|
129
|
+
|
|
130
|
+
const { agentId, error } = resolveAgentId(name);
|
|
131
|
+
if (error) return fail(error);
|
|
132
|
+
|
|
133
|
+
const spinner = villageSpinner(`Loading credential grants for "${name}"...`).start();
|
|
134
|
+
try {
|
|
135
|
+
const result = await listAgentCredentialGrants(agentId);
|
|
136
|
+
const grants = result?.data || result || [];
|
|
137
|
+
spinner.stop();
|
|
138
|
+
|
|
139
|
+
if (!Array.isArray(grants) || grants.length === 0) {
|
|
140
|
+
console.log(brand.teal(`\n "${name}" has no active credential grants.`));
|
|
141
|
+
console.log(brand.teal(` Authorize one: ${chalk.bold(`myvillage agent grant ${name} <google|microsoft|zoom>`)}\n`));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log('');
|
|
146
|
+
console.log(chalk.bold(` Credential grants for "${name}"`));
|
|
147
|
+
console.log('');
|
|
148
|
+
for (const g of grants) {
|
|
149
|
+
const slug = String(g.provider || '').toLowerCase();
|
|
150
|
+
const label = PROVIDER_LABELS[slug] || g.provider;
|
|
151
|
+
const when = g.grantedAt ? ` ${chalk.dim(`(granted ${new Date(g.grantedAt).toLocaleDateString()})`)}` : '';
|
|
152
|
+
console.log(` ${brand.green('●')} ${label}${when}`);
|
|
153
|
+
}
|
|
154
|
+
console.log('');
|
|
155
|
+
} catch (err) {
|
|
156
|
+
spinner.fail(`Failed to load grants: ${err.response?.data?.error || err.message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
package/src/index.js
CHANGED
|
@@ -84,6 +84,11 @@ import {
|
|
|
84
84
|
agentListMcpCommand,
|
|
85
85
|
recipesListCommand,
|
|
86
86
|
} from './commands/agent-mcp.js';
|
|
87
|
+
import {
|
|
88
|
+
agentGrantCommand,
|
|
89
|
+
agentRevokeCredentialCommand,
|
|
90
|
+
agentGrantsListCommand,
|
|
91
|
+
} from './commands/agent-credentials.js';
|
|
87
92
|
import {
|
|
88
93
|
gameUpdateCommand,
|
|
89
94
|
gameUploadThumbnailCommand,
|
|
@@ -553,6 +558,24 @@ export function run() {
|
|
|
553
558
|
.description('List the MCP servers currently configured for an agent')
|
|
554
559
|
.action(agentListMcpCommand);
|
|
555
560
|
|
|
561
|
+
// Per-agent OAuth credential grants (Google/Microsoft/Zoom). The villager
|
|
562
|
+
// connects the provider once in the app/portal; these authorize a specific
|
|
563
|
+
// agent to use it via the MyVillage MCP server. Tokens never touch the CLI.
|
|
564
|
+
agentCmd
|
|
565
|
+
.command('grant <name> <provider>')
|
|
566
|
+
.description('Authorize an agent to use a connected provider (google, microsoft, zoom)')
|
|
567
|
+
.action(agentGrantCommand);
|
|
568
|
+
|
|
569
|
+
agentCmd
|
|
570
|
+
.command('revoke <name> <provider>')
|
|
571
|
+
.description('Revoke an agent\'s access to a provider (google, microsoft, zoom)')
|
|
572
|
+
.action(agentRevokeCredentialCommand);
|
|
573
|
+
|
|
574
|
+
agentCmd
|
|
575
|
+
.command('grants <name>')
|
|
576
|
+
.description('List an agent\'s active OAuth credential grants')
|
|
577
|
+
.action(agentGrantsListCommand);
|
|
578
|
+
|
|
556
579
|
program
|
|
557
580
|
.command('recipes')
|
|
558
581
|
.description('List the MCP recipes bundled with the CLI')
|
package/src/utils/api.js
CHANGED
|
@@ -777,3 +777,31 @@ export async function shareKnowledgeAsAgent(data) {
|
|
|
777
777
|
const response = await client.post('/agent-tools/share-knowledge', data);
|
|
778
778
|
return response.data;
|
|
779
779
|
}
|
|
780
|
+
|
|
781
|
+
// ── Agent OAuth credential grants ───────────────────────
|
|
782
|
+
//
|
|
783
|
+
// Per-agent authorization to use the villager's connected providers
|
|
784
|
+
// (GOOGLE / MICROSOFT / ZOOM) when the agent runs through the MyVillage MCP
|
|
785
|
+
// server. The platform vends short-lived tokens to the MCP server — the raw
|
|
786
|
+
// OAuth tokens never reach the CLI. `agentId` is the AgentProfile id, i.e.
|
|
787
|
+
// the local agent's `man.agent_id` (populated on first `agent start`).
|
|
788
|
+
//
|
|
789
|
+
// Endpoints live under platformBaseUrl (/api), not /api/v1.
|
|
790
|
+
|
|
791
|
+
export async function listAgentCredentialGrants(agentId) {
|
|
792
|
+
const client = getPlatformClient();
|
|
793
|
+
const response = await client.get(`/agents/${agentId}/credential-grants`);
|
|
794
|
+
return response.data;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
export async function grantAgentCredential(agentId, provider) {
|
|
798
|
+
const client = getPlatformClient();
|
|
799
|
+
const response = await client.post(`/agents/${agentId}/credential-grants`, { provider });
|
|
800
|
+
return response.data;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export async function revokeAgentCredential(agentId, provider) {
|
|
804
|
+
const client = getPlatformClient();
|
|
805
|
+
const response = await client.delete(`/agents/${agentId}/credential-grants/${provider}`);
|
|
806
|
+
return response.data;
|
|
807
|
+
}
|