@myvillage/cli 1.18.0 → 1.21.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/api-key.js +172 -0
- package/src/index.js +35 -0
- package/src/utils/api.js +26 -0
package/package.json
CHANGED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { villageSpinner, brand } from '../utils/brand.js';
|
|
4
|
+
import { isAuthenticated } from '../utils/auth.js';
|
|
5
|
+
import {
|
|
6
|
+
createApiKey,
|
|
7
|
+
listApiKeys,
|
|
8
|
+
revokeApiKey,
|
|
9
|
+
listAllowedScopes,
|
|
10
|
+
} from '../utils/api.js';
|
|
11
|
+
|
|
12
|
+
// ── create ──────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export async function apiKeyCreateCommand(options = {}) {
|
|
15
|
+
if (!isAuthenticated()) {
|
|
16
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!options.name) {
|
|
21
|
+
console.log(chalk.red(' ✗ --name is required (e.g. --name "claude-desktop")\n'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let scopes;
|
|
26
|
+
if (options.scopes) {
|
|
27
|
+
scopes = options.scopes.split(',').map(s => s.trim()).filter(Boolean);
|
|
28
|
+
} else {
|
|
29
|
+
// Interactive picker — fetch what the caller is allowed to mint
|
|
30
|
+
const spinner = villageSpinner('Loading available scopes...').start();
|
|
31
|
+
let allowed;
|
|
32
|
+
try {
|
|
33
|
+
const result = await listAllowedScopes();
|
|
34
|
+
allowed = result.allowedScopes || {};
|
|
35
|
+
spinner.stop();
|
|
36
|
+
} catch (err) {
|
|
37
|
+
spinner.fail(`Failed to load scopes: ${err.response?.data?.error || err.message}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const scopeKeys = Object.keys(allowed);
|
|
41
|
+
if (scopeKeys.length === 0) {
|
|
42
|
+
console.log(chalk.yellow(' ⚠ Your account has no API scopes available.\n'));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const { picked } = await inquirer.prompt([{
|
|
46
|
+
type: 'checkbox',
|
|
47
|
+
name: 'picked',
|
|
48
|
+
message: 'Pick the scopes this key should carry (Space to toggle, Enter to confirm):',
|
|
49
|
+
pageSize: Math.min(scopeKeys.length, 15),
|
|
50
|
+
choices: scopeKeys.map(s => ({
|
|
51
|
+
name: `${s} ${chalk.dim('— ' + allowed[s])}`,
|
|
52
|
+
value: s,
|
|
53
|
+
checked: s === 'agents:tasks:read' || s === 'agents:tasks:write',
|
|
54
|
+
})),
|
|
55
|
+
validate: (input) => input.length > 0 || 'Pick at least one scope.',
|
|
56
|
+
}]);
|
|
57
|
+
scopes = picked;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const body = { name: options.name, scopes };
|
|
61
|
+
if (options.expiresInDays) {
|
|
62
|
+
body.expiresInDays = Number(options.expiresInDays);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const spinner = villageSpinner('Creating API key...').start();
|
|
66
|
+
try {
|
|
67
|
+
const result = await createApiKey(body);
|
|
68
|
+
spinner.succeed('API key created.');
|
|
69
|
+
|
|
70
|
+
console.log(brand.teal('\n Key (shown ONCE — copy it now):\n'));
|
|
71
|
+
console.log(' ' + chalk.bold(result.key));
|
|
72
|
+
console.log('');
|
|
73
|
+
console.log(brand.teal(` Id: ${result.apiKey.id}`));
|
|
74
|
+
console.log(brand.teal(` Prefix: ${result.apiKey.prefix}`));
|
|
75
|
+
console.log(brand.teal(` Scopes: ${result.apiKey.scopes.join(', ')}`));
|
|
76
|
+
if (result.apiKey.expiresAt) {
|
|
77
|
+
console.log(brand.teal(` Expires: ${result.apiKey.expiresAt}`));
|
|
78
|
+
} else {
|
|
79
|
+
console.log(brand.teal(' Expires: never (revoke manually when no longer needed)'));
|
|
80
|
+
}
|
|
81
|
+
console.log(chalk.yellow('\n ⚠ Treat this key like a password. Anyone with it can act as you'));
|
|
82
|
+
console.log(chalk.yellow(' within the scopes you selected. Store it in an env var, not in git.\n'));
|
|
83
|
+
console.log(brand.teal(' Use in REST calls:'));
|
|
84
|
+
console.log(' ' + chalk.dim(`curl -H "Authorization: Bearer ${result.key.slice(0, 12)}..." https://portal.myvillageproject.ai/api/...\n`));
|
|
85
|
+
console.log(brand.teal(' Use in Claude Desktop / Cursor MCP config:'));
|
|
86
|
+
console.log(' ' + chalk.dim(`"headers": { "Authorization": "Bearer ${result.key.slice(0, 12)}..." }\n`));
|
|
87
|
+
} catch (err) {
|
|
88
|
+
const msg = err.response?.data?.error || err.message;
|
|
89
|
+
spinner.fail(`Failed to create API key: ${msg}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── list ────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
export async function apiKeyListCommand() {
|
|
96
|
+
if (!isAuthenticated()) {
|
|
97
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const result = await listApiKeys();
|
|
103
|
+
const keys = result.apiKeys || [];
|
|
104
|
+
if (keys.length === 0) {
|
|
105
|
+
console.log(brand.teal(' No API keys yet. Create one with: myvillage api-key create --name "..."\n'));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
console.log(brand.teal(`\n ${keys.length} API key(s):\n`));
|
|
109
|
+
for (const k of keys) {
|
|
110
|
+
const status = k.isActive ? brand.green('active') : chalk.dim('revoked');
|
|
111
|
+
console.log(` ${chalk.bold(k.name)} ${status}`);
|
|
112
|
+
console.log(` id: ${k.id}`);
|
|
113
|
+
console.log(` prefix: ${k.prefix}`);
|
|
114
|
+
console.log(` scopes: ${(k.scopes || []).join(', ')}`);
|
|
115
|
+
if (k.lastUsedAt) console.log(` used: ${k.lastUsedAt}`);
|
|
116
|
+
if (k.expiresAt) console.log(` expires: ${k.expiresAt}`);
|
|
117
|
+
console.log('');
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
const msg = err.response?.data?.error || err.message;
|
|
121
|
+
console.log(chalk.red(` ✗ Failed to list API keys: ${msg}\n`));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── revoke ──────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
export async function apiKeyRevokeCommand(id) {
|
|
128
|
+
if (!isAuthenticated()) {
|
|
129
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!id) {
|
|
133
|
+
console.log(chalk.red(' ✗ Usage: myvillage api-key revoke <id>\n'));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
await revokeApiKey(id);
|
|
139
|
+
console.log(brand.green(` ✓ API key ${id} revoked. Any further requests using it will fail.\n`));
|
|
140
|
+
} catch (err) {
|
|
141
|
+
const msg = err.response?.data?.error || err.message;
|
|
142
|
+
console.log(chalk.red(` ✗ Failed to revoke API key: ${msg}\n`));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── show-scopes ─────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
export async function apiKeyShowScopesCommand() {
|
|
149
|
+
if (!isAuthenticated()) {
|
|
150
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const result = await listAllowedScopes();
|
|
156
|
+
const allowed = result.allowedScopes || {};
|
|
157
|
+
const keys = Object.keys(allowed);
|
|
158
|
+
if (keys.length === 0) {
|
|
159
|
+
console.log(brand.teal(' No API scopes available for your account.\n'));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
console.log(brand.teal(`\n ${keys.length} scope(s) available to you:\n`));
|
|
163
|
+
for (const k of keys) {
|
|
164
|
+
console.log(` ${chalk.bold(k)}`);
|
|
165
|
+
console.log(` ${chalk.dim(allowed[k])}`);
|
|
166
|
+
}
|
|
167
|
+
console.log('');
|
|
168
|
+
} catch (err) {
|
|
169
|
+
const msg = err.response?.data?.error || err.message;
|
|
170
|
+
console.log(chalk.red(` ✗ Failed to load allowed scopes: ${msg}\n`));
|
|
171
|
+
}
|
|
172
|
+
}
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,12 @@ import {
|
|
|
9
9
|
mediaDraftStatusCommand,
|
|
10
10
|
} from './commands/media.js';
|
|
11
11
|
import { logoutCommand } from './commands/logout.js';
|
|
12
|
+
import {
|
|
13
|
+
apiKeyCreateCommand,
|
|
14
|
+
apiKeyListCommand,
|
|
15
|
+
apiKeyRevokeCommand,
|
|
16
|
+
apiKeyShowScopesCommand,
|
|
17
|
+
} from './commands/api-key.js';
|
|
12
18
|
import { createGameCommand } from './commands/create-game.js';
|
|
13
19
|
import { createCommand } from './commands/create-app.js';
|
|
14
20
|
import { deployCommand } from './commands/deploy.js';
|
|
@@ -141,6 +147,35 @@ export function run() {
|
|
|
141
147
|
.description('Clear stored credentials')
|
|
142
148
|
.action(logoutCommand);
|
|
143
149
|
|
|
150
|
+
// ── API Keys (developer self-service) ──────────────────
|
|
151
|
+
|
|
152
|
+
const apiKeyCmd = program
|
|
153
|
+
.command('api-key')
|
|
154
|
+
.description('Manage your API keys (used for REST and MCP authentication)');
|
|
155
|
+
|
|
156
|
+
apiKeyCmd
|
|
157
|
+
.command('create')
|
|
158
|
+
.description('Create a new API key. Shown ONCE — copy it immediately.')
|
|
159
|
+
.requiredOption('--name <name>', 'Friendly name for this key (e.g. "claude-desktop")')
|
|
160
|
+
.option('--scopes <list>', 'Comma-separated scopes (omit for interactive picker)')
|
|
161
|
+
.option('--expires-in-days <n>', 'Number of days until expiry (default: never)')
|
|
162
|
+
.action(apiKeyCreateCommand);
|
|
163
|
+
|
|
164
|
+
apiKeyCmd
|
|
165
|
+
.command('list')
|
|
166
|
+
.description('List your API keys (names, prefixes, scopes — never the secret)')
|
|
167
|
+
.action(apiKeyListCommand);
|
|
168
|
+
|
|
169
|
+
apiKeyCmd
|
|
170
|
+
.command('revoke <id>')
|
|
171
|
+
.description('Revoke an API key by id so further requests using it are rejected')
|
|
172
|
+
.action(apiKeyRevokeCommand);
|
|
173
|
+
|
|
174
|
+
apiKeyCmd
|
|
175
|
+
.command('show-scopes')
|
|
176
|
+
.description('Print the API scopes your account is allowed to mint into a key')
|
|
177
|
+
.action(apiKeyShowScopesCommand);
|
|
178
|
+
|
|
144
179
|
program
|
|
145
180
|
.command('create-game')
|
|
146
181
|
.description('Create a new game project with interactive wizard')
|
package/src/utils/api.js
CHANGED
|
@@ -701,6 +701,32 @@ export async function retryFailedAgentTasks(villageAgentId, errorPattern) {
|
|
|
701
701
|
return response.data;
|
|
702
702
|
}
|
|
703
703
|
|
|
704
|
+
// ── API Key management ─────────────────────────────────
|
|
705
|
+
|
|
706
|
+
export async function createApiKey(data) {
|
|
707
|
+
const client = getPlatformClient();
|
|
708
|
+
const response = await client.post('/api-keys', data);
|
|
709
|
+
return response.data;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
export async function listApiKeys() {
|
|
713
|
+
const client = getPlatformClient();
|
|
714
|
+
const response = await client.get('/api-keys');
|
|
715
|
+
return response.data;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export async function revokeApiKey(id) {
|
|
719
|
+
const client = getPlatformClient();
|
|
720
|
+
const response = await client.delete(`/api-keys/${encodeURIComponent(id)}`);
|
|
721
|
+
return response.data;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
export async function listAllowedScopes() {
|
|
725
|
+
const client = getPlatformClient();
|
|
726
|
+
const response = await client.get('/api-keys/allowed-scopes');
|
|
727
|
+
return response.data;
|
|
728
|
+
}
|
|
729
|
+
|
|
704
730
|
// ── Wisdom (VillageBooks repurposed as agent skill packs) ──────────
|
|
705
731
|
|
|
706
732
|
export async function listVillageBooks(params = {}) {
|