@myvillage/cli 1.18.0 → 1.22.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.22.0",
4
4
  "description": "MyVillageOS CLI for community developers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -152,8 +152,18 @@ export async function agentLoop(agentName, { signal }) {
152
152
  mentionsFound = contextResult.mentionsCount;
153
153
 
154
154
  if (activeTask) {
155
- const taskLine = `TASK ${activeTask.id} (${activeTask.taskType}): ${activeTask.instruction || JSON.stringify(activeTask.input || {})}`;
156
- context = `${taskLine}\n\n${context}`;
155
+ // A claimed task takes over the iteration. The default monitoring
156
+ // context becomes secondary background; the instruction is the
157
+ // primary user prompt and the system prompt switches to a
158
+ // task-execution framing so the LLM doesn't drift back into
159
+ // "report on the feed" mode.
160
+ const instructionText = activeTask.instruction
161
+ || (activeTask.input ? JSON.stringify(activeTask.input, null, 2) : '');
162
+
163
+ systemPrompt = `${systemPrompt}\n\n## ACTIVE TASK MODE\nA client has assigned you a task. Your job this iteration is to execute the instruction below using your available tools. The feed context is provided only for situational awareness — do not let "no new feed activity" prevent you from carrying out the task.`;
164
+
165
+ context = `## TASK (id=${activeTask.id}, type=${activeTask.taskType})\n${instructionText}\n\n---\n\n## FEED CONTEXT (for awareness only)\n${context}`;
166
+
157
167
  logActivity(agentDir, { type: 'task_claimed', taskId: activeTask.id, taskType: activeTask.taskType });
158
168
  }
159
169
 
@@ -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 = {}) {