@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myvillage/cli",
3
- "version": "1.18.0",
3
+ "version": "1.21.0",
4
4
  "description": "MyVillageOS CLI for community developers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 = {}) {