@myvillage/cli 1.10.1 → 1.17.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.
@@ -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
+ }
@@ -12,6 +12,18 @@ import {
12
12
  agentJoinCommunity as apiAgentJoinCommunity,
13
13
  listCommunities,
14
14
  getAgentActivity,
15
+ listMyUnlinkedAgentProfiles,
16
+ createVillageAgent as apiCreateVillageAgent,
17
+ listAgentTasks,
18
+ assignAgentTask,
19
+ claimAgentTask,
20
+ completeAgentTask,
21
+ upsertAgentMemory,
22
+ listAgentMemory,
23
+ getAgentMemoryEntry,
24
+ deleteAgentMemoryEntry,
25
+ listKnowledgeFiltered,
26
+ shareKnowledgeAsAgent,
15
27
  } from '../utils/api.js';
16
28
  import {
17
29
  getAgentDir,
@@ -39,6 +51,36 @@ export async function agentCreateLocalCommand() {
39
51
  }
40
52
 
41
53
  try {
54
+ // Optional: let the developer reuse an existing AgentProfile (network identity)
55
+ // they already own and haven't yet linked to a VillageAgent.
56
+ let preselectedProfile = null;
57
+ try {
58
+ const result = await listMyUnlinkedAgentProfiles();
59
+ const profiles = Array.isArray(result?.data) ? result.data : [];
60
+ if (profiles.length > 0) {
61
+ const { useExisting } = await inquirer.prompt([{
62
+ type: 'confirm',
63
+ name: 'useExisting',
64
+ message: `You have ${profiles.length} unlinked network identity(ies). Reuse one for this agent?`,
65
+ default: false,
66
+ }]);
67
+ if (useExisting) {
68
+ const { profileId } = await inquirer.prompt([{
69
+ type: 'list',
70
+ name: 'profileId',
71
+ message: 'Pick a network identity:',
72
+ choices: profiles.map(p => ({
73
+ name: `@${p.handle} — ${p.displayName}`,
74
+ value: p.id,
75
+ })),
76
+ }]);
77
+ preselectedProfile = profiles.find(p => p.id === profileId) || null;
78
+ }
79
+ }
80
+ } catch {
81
+ // listMyUnlinkedAgentProfiles can fail if the user has no villager — that's fine, just skip
82
+ }
83
+
42
84
  const answers = await inquirer.prompt([
43
85
  {
44
86
  type: 'input',
@@ -151,6 +193,16 @@ export async function agentCreateLocalCommand() {
151
193
  model: answers.model,
152
194
  });
153
195
 
196
+ // If the developer picked an existing AgentProfile, pre-fill man.agent_id
197
+ // so `start` skips the lazy-create step and links the VillageAgent shim to it instead.
198
+ if (preselectedProfile?.id) {
199
+ const cfg = readAgentConfig(answers.name);
200
+ cfg.man = cfg.man || {};
201
+ cfg.man.agent_id = preselectedProfile.id;
202
+ cfg.man.handle = preselectedProfile.handle;
203
+ writeAgentConfig(answers.name, cfg);
204
+ }
205
+
154
206
  spinner.succeed('Agent scaffolded!');
155
207
 
156
208
  console.log(brand.green(` \u2713 Agent created at ~/.myvillage/agents/${answers.name}/\n`));
@@ -231,7 +283,7 @@ export async function agentStartCommand(name) {
231
283
  }
232
284
  }
233
285
 
234
- // Register on MAN if first start
286
+ // Register on MAN if first start (skipped when `create` preselected an existing AgentProfile)
235
287
  if (!agentConfig.man?.agent_id) {
236
288
  const regSpinner = villageSpinner('Registering agent on the MAN network...').start();
237
289
  try {
@@ -251,6 +303,32 @@ export async function agentStartCommand(name) {
251
303
 
252
304
  regSpinner.succeed(`Registered as @${agent.handle || agentConfig.name} on MAN.`);
253
305
 
306
+ // Also create the VillageAgent shim so this agent has a task queue
307
+ // and shared autonomous-engagement controls. This is what `loop.js`
308
+ // polls against. Failure is non-fatal — the agent can still run locally.
309
+ try {
310
+ const shimResult = await apiCreateVillageAgent({
311
+ name: agentConfig.display_name || agentConfig.name,
312
+ title: 'Developer Agent',
313
+ description: agentConfig.description || '',
314
+ avatar: agent.avatarUrl || '',
315
+ archetypes: [],
316
+ greeting: '',
317
+ starterPrompts: [],
318
+ associatedVillages: [],
319
+ specialInstructions: null,
320
+ agentProfileId: agent.id,
321
+ });
322
+ const shim = shimResult.agent || shimResult;
323
+ if (shim?.id) {
324
+ agentConfig.man.village_agent_id = shim.id;
325
+ writeAgentConfig(name, agentConfig);
326
+ }
327
+ } catch (shimErr) {
328
+ const shimMsg = shimErr.response?.data?.error || shimErr.message;
329
+ console.log(brand.teal(` Note: task queue not available (${shimMsg}). Agent will run in feed-monitoring mode.`));
330
+ }
331
+
254
332
  // Auto-join default communities so the agent can post
255
333
  try {
256
334
  const commResult = await listCommunities({ pageSize: 50 });
@@ -284,6 +362,34 @@ export async function agentStartCommand(name) {
284
362
  }
285
363
  }
286
364
 
365
+ // If we have an AgentProfile but no VillageAgent shim yet (e.g., the developer
366
+ // preselected an existing AgentProfile via `create`), create it now.
367
+ if (agentConfig.man?.agent_id && !agentConfig.man?.village_agent_id) {
368
+ try {
369
+ const shimResult = await apiCreateVillageAgent({
370
+ name: agentConfig.display_name || agentConfig.name,
371
+ title: 'Developer Agent',
372
+ description: agentConfig.description || '',
373
+ avatar: '',
374
+ archetypes: [],
375
+ greeting: '',
376
+ starterPrompts: [],
377
+ associatedVillages: [],
378
+ specialInstructions: null,
379
+ agentProfileId: agentConfig.man.agent_id,
380
+ });
381
+ const shim = shimResult.agent || shimResult;
382
+ if (shim?.id) {
383
+ agentConfig.man.village_agent_id = shim.id;
384
+ writeAgentConfig(name, agentConfig);
385
+ console.log(brand.teal(` Task queue ready: ${shim.id}`));
386
+ }
387
+ } catch (shimErr) {
388
+ const shimMsg = shimErr.response?.data?.error || shimErr.message;
389
+ console.log(brand.teal(` Note: task queue not available (${shimMsg}).`));
390
+ }
391
+ }
392
+
287
393
  // Migrate tools.yaml: replace man-feed with myvillage MCP server
288
394
  const toolsConfig = readToolsYaml(name);
289
395
  if (toolsConfig.servers?.['man-feed'] && !toolsConfig.servers?.['myvillage']) {
@@ -300,6 +406,9 @@ export async function agentStartCommand(name) {
300
406
  if (agentConfig.man?.agent_id) {
301
407
  process.env.MYVILLAGE_AGENT_ID = agentConfig.man.agent_id;
302
408
  }
409
+ if (agentConfig.man?.village_agent_id) {
410
+ process.env.MYVILLAGE_VILLAGE_AGENT_ID = agentConfig.man.village_agent_id;
411
+ }
303
412
 
304
413
  // Fork the daemon process
305
414
  const spinner = villageSpinner(`Starting agent "${name}"...`).start();
@@ -670,3 +779,247 @@ export async function agentDeleteLocalCommand(name) {
670
779
  console.log(chalk.red(` \u2717 Failed to delete agent: ${err.message}\n`));
671
780
  }
672
781
  }
782
+
783
+ // \u2500\u2500 Tasks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
784
+
785
+ function resolveVillageAgentId(name) {
786
+ if (!agentExists(name)) {
787
+ console.log(chalk.red(` \u2717 Agent "${name}" not found.\n`));
788
+ return null;
789
+ }
790
+ const config = readAgentConfig(name);
791
+ const id = config?.man?.village_agent_id;
792
+ if (!id) {
793
+ console.log(chalk.yellow(` Agent "${name}" has no task queue yet \u2014 start it once so the VillageAgent shim is created.\n`));
794
+ return null;
795
+ }
796
+ return id;
797
+ }
798
+
799
+ export async function agentTaskListCommand(name, options = {}) {
800
+ if (!isAuthenticated()) {
801
+ console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
802
+ return;
803
+ }
804
+ const villageAgentId = resolveVillageAgentId(name);
805
+ if (!villageAgentId) return;
806
+
807
+ try {
808
+ const result = await listAgentTasks(villageAgentId, {
809
+ status: options.status,
810
+ limit: options.limit ? Number(options.limit) : undefined,
811
+ });
812
+ const tasks = result.tasks || [];
813
+ if (tasks.length === 0) {
814
+ console.log(brand.teal(` No tasks${options.status ? ` with status ${options.status}` : ''}.\n`));
815
+ return;
816
+ }
817
+ console.log(brand.teal(` ${tasks.length} task(s):\n`));
818
+ for (const t of tasks) {
819
+ console.log(` ${brand.gold(t.id)} ${chalk.bold(t.taskType)} ${t.status} priority=${t.priority}`);
820
+ if (t.instruction) console.log(` ${chalk.dim(t.instruction)}`);
821
+ }
822
+ console.log('');
823
+ } catch (err) {
824
+ const msg = err.response?.data?.error || err.message;
825
+ console.log(chalk.red(` \u2717 Failed to list tasks: ${msg}\n`));
826
+ }
827
+ }
828
+
829
+ export async function agentTaskAssignCommand(name, options = {}) {
830
+ if (!isAuthenticated()) {
831
+ console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
832
+ return;
833
+ }
834
+ const villageAgentId = resolveVillageAgentId(name);
835
+ if (!villageAgentId) return;
836
+
837
+ if (!options.type) {
838
+ console.log(chalk.red(' \u2717 --type is required (e.g., CLIENT_TASK, GENERATE_POST, SHARE_KNOWLEDGE)\n'));
839
+ return;
840
+ }
841
+
842
+ let inputObj = {};
843
+ if (options.input) {
844
+ try { inputObj = JSON.parse(options.input); }
845
+ catch { console.log(chalk.red(' \u2717 --input must be valid JSON\n')); return; }
846
+ }
847
+
848
+ try {
849
+ const result = await assignAgentTask(villageAgentId, {
850
+ taskType: options.type,
851
+ instruction: options.instruction,
852
+ input: inputObj,
853
+ priority: options.priority ? Number(options.priority) : 5,
854
+ });
855
+ const task = result.task || result;
856
+ console.log(brand.green(` \u2713 Task ${task.id} assigned (${task.taskType}, status=${task.status}).\n`));
857
+ } catch (err) {
858
+ const msg = err.response?.data?.error || err.message;
859
+ console.log(chalk.red(` \u2717 Failed to assign task: ${msg}\n`));
860
+ }
861
+ }
862
+
863
+ // Exported for use by the runtime loop, not registered as a CLI command directly.
864
+ export async function pollAndClaimNextTask(villageAgentId) {
865
+ const result = await listAgentTasks(villageAgentId, { status: 'PENDING', limit: 5 });
866
+ const pending = result.tasks || [];
867
+ if (pending.length === 0) return null;
868
+ for (const task of pending) {
869
+ try {
870
+ const claim = await claimAgentTask(villageAgentId, task.id);
871
+ return claim.data || claim;
872
+ } catch {
873
+ // Race lost \u2014 try the next task
874
+ }
875
+ }
876
+ return null;
877
+ }
878
+
879
+ export async function reportTaskOutcome(villageAgentId, taskId, outcome) {
880
+ return completeAgentTask(villageAgentId, taskId, outcome);
881
+ }
882
+
883
+ // ── Memory (short-term KV) and Recall (long-term searchable) ────
884
+
885
+ function resolveAgentProfileId(name) {
886
+ if (!agentExists(name)) {
887
+ console.log(chalk.red(` ✗ Agent "${name}" not found.\n`));
888
+ return null;
889
+ }
890
+ const config = readAgentConfig(name);
891
+ const id = config?.man?.agent_id;
892
+ if (!id) {
893
+ console.log(chalk.yellow(` Agent "${name}" hasn't registered on the network yet — start it once so an AgentProfile is created.\n`));
894
+ return null;
895
+ }
896
+ return id;
897
+ }
898
+
899
+ export async function agentMemoryCommand(name, action, ...args) {
900
+ if (!isAuthenticated()) {
901
+ console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
902
+ return;
903
+ }
904
+ const agentProfileId = resolveAgentProfileId(name);
905
+ if (!agentProfileId) return;
906
+
907
+ try {
908
+ if (action === 'list') {
909
+ const result = await listAgentMemory(agentProfileId);
910
+ const items = result.data || result.memories || result;
911
+ if (!Array.isArray(items) || items.length === 0) {
912
+ console.log(brand.teal(' No memory entries.\n'));
913
+ return;
914
+ }
915
+ console.log(brand.teal(` ${items.length} memory entr${items.length === 1 ? 'y' : 'ies'}:\n`));
916
+ for (const m of items) {
917
+ console.log(` ${chalk.bold(m.key)}${m.category ? chalk.dim(` [${m.category}]`) : ''}`);
918
+ const v = typeof m.value === 'string' ? m.value : JSON.stringify(m.value);
919
+ console.log(` ${chalk.dim(v.slice(0, 200))}`);
920
+ }
921
+ console.log('');
922
+ } else if (action === 'get') {
923
+ const key = args[0];
924
+ if (!key) {
925
+ console.log(chalk.red(' ✗ Usage: myvillage agent memory <name> get <key>\n'));
926
+ return;
927
+ }
928
+ const result = await getAgentMemoryEntry(agentProfileId, key);
929
+ console.log(JSON.stringify(result.data || result, null, 2));
930
+ } else if (action === 'set') {
931
+ const [key, ...rest] = args;
932
+ const value = rest.join(' ');
933
+ if (!key || !value) {
934
+ console.log(chalk.red(' ✗ Usage: myvillage agent memory <name> set <key> <value>\n'));
935
+ return;
936
+ }
937
+ // Try JSON first so { "foo": 1 } stays structured; fall back to string.
938
+ let parsed;
939
+ try { parsed = JSON.parse(value); } catch { parsed = value; }
940
+ await upsertAgentMemory(agentProfileId, { key, value: parsed });
941
+ console.log(brand.green(` ✓ ${key} set.\n`));
942
+ } else if (action === 'delete') {
943
+ const key = args[0];
944
+ if (!key) {
945
+ console.log(chalk.red(' ✗ Usage: myvillage agent memory <name> delete <key>\n'));
946
+ return;
947
+ }
948
+ await deleteAgentMemoryEntry(agentProfileId, key);
949
+ console.log(brand.green(` ✓ ${key} deleted.\n`));
950
+ } else {
951
+ console.log(chalk.red(` ✗ Unknown memory action: ${action}. Use list|get|set|delete.\n`));
952
+ }
953
+ } catch (err) {
954
+ const msg = err.response?.data?.error || err.message;
955
+ console.log(chalk.red(` ✗ Memory ${action} failed: ${msg}\n`));
956
+ }
957
+ }
958
+
959
+ export async function agentRecallCommand(name, query, options = {}) {
960
+ if (!isAuthenticated()) {
961
+ console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
962
+ return;
963
+ }
964
+ const agentProfileId = resolveAgentProfileId(name);
965
+ if (!agentProfileId) return;
966
+ if (!query) {
967
+ console.log(chalk.red(' ✗ Usage: myvillage agent recall <name> <query>\n'));
968
+ return;
969
+ }
970
+
971
+ try {
972
+ const result = await listKnowledgeFiltered({
973
+ createdByAgentId: agentProfileId,
974
+ search: query,
975
+ limit: options.limit ? Number(options.limit) : 10,
976
+ });
977
+ const items = result.data || result.knowledge || result.knowledgeSubmissions || [];
978
+ if (!Array.isArray(items) || items.length === 0) {
979
+ console.log(brand.teal(` No memories matched "${query}".\n`));
980
+ return;
981
+ }
982
+ console.log(brand.teal(` ${items.length} matching memor${items.length === 1 ? 'y' : 'ies'}:\n`));
983
+ for (const k of items) {
984
+ const text = k.originalText || k.text || '';
985
+ console.log(` ${brand.gold(k.id)} ${chalk.dim(new Date(k.createdAt).toLocaleString())}`);
986
+ console.log(` ${text.slice(0, 280)}${text.length > 280 ? '…' : ''}`);
987
+ }
988
+ console.log('');
989
+ } catch (err) {
990
+ const msg = err.response?.data?.error || err.message;
991
+ console.log(chalk.red(` ✗ Recall failed: ${msg}\n`));
992
+ }
993
+ }
994
+
995
+ export async function agentRememberCommand(name, text, options = {}) {
996
+ if (!isAuthenticated()) {
997
+ console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
998
+ return;
999
+ }
1000
+ const agentProfileId = resolveAgentProfileId(name);
1001
+ if (!agentProfileId) return;
1002
+ if (!text) {
1003
+ console.log(chalk.red(' ✗ Usage: myvillage agent remember <name> "<text to remember>"\n'));
1004
+ return;
1005
+ }
1006
+
1007
+ const themes = options.themes
1008
+ ? options.themes.split(',').map(t => t.trim()).filter(Boolean)
1009
+ : [];
1010
+
1011
+ try {
1012
+ const result = await shareKnowledgeAsAgent({
1013
+ original_text: text,
1014
+ summary: options.summary || null,
1015
+ themes,
1016
+ sharing_option: options.sharing || 'PRIVATE',
1017
+ agent_profile_id: agentProfileId,
1018
+ });
1019
+ const k = result.knowledge || result.data || result;
1020
+ console.log(brand.green(` ✓ Remembered as ${k.id || 'knowledge entry'}.\n`));
1021
+ } catch (err) {
1022
+ const msg = err.response?.data?.error || err.message;
1023
+ console.log(chalk.red(` ✗ Remember failed: ${msg}\n`));
1024
+ }
1025
+ }
@@ -7,7 +7,7 @@ import inquirer from 'inquirer';
7
7
  import { isAuthenticated } from '../utils/auth.js';
8
8
  import { createGameCommand } from './create-game.js';
9
9
  import { createAgenticAppProject } from '../utils/agentic-templates.js';
10
- import { registerOAuthClient } from '../utils/api.js';
10
+ import { registerOAuthClient, registerClientAgent, listMyAgents } from '../utils/api.js';
11
11
 
12
12
  export async function createCommand() {
13
13
  // Check authentication
@@ -160,6 +160,65 @@ async function applicationFlow() {
160
160
  }
161
161
  }
162
162
 
163
+ // Client agent automation (optional)
164
+ let agentConfig = null;
165
+ if (hasOAuth && oauthCredentials) {
166
+ const { enableAgent } = await inquirer.prompt([{
167
+ type: 'confirm',
168
+ name: 'enableAgent',
169
+ message: 'Enable platform agent automation?',
170
+ default: false,
171
+ }]);
172
+
173
+ if (enableAgent) {
174
+ try {
175
+ const agentResult = await listMyAgents();
176
+ const agents = agentResult.data || agentResult;
177
+
178
+ if (Array.isArray(agents) && agents.length > 0) {
179
+ const { selectedAgent, workflowTypes } = await inquirer.prompt([
180
+ {
181
+ type: 'list',
182
+ name: 'selectedAgent',
183
+ message: 'Which agent should automate this app?',
184
+ choices: agents.map(a => ({ name: `@${a.handle} — ${a.displayName || a.handle}`, value: a })),
185
+ },
186
+ {
187
+ type: 'checkbox',
188
+ name: 'workflowTypes',
189
+ message: 'Select workflow types:',
190
+ choices: [
191
+ { name: 'Submission Processor', value: 'SUBMISSION_PROCESSOR', checked: true },
192
+ { name: 'Digest Generator', value: 'DIGEST_GENERATOR' },
193
+ { name: 'Member Matcher', value: 'MEMBER_MATCHER' },
194
+ { name: 'Stale Data Detector', value: 'STALE_DATA_DETECTOR' },
195
+ ],
196
+ validate: input => input.length > 0 ? true : 'Select at least one workflow type.',
197
+ },
198
+ ]);
199
+
200
+ const agentSpinner = villageSpinner('Registering client agent...').start();
201
+ const regResult = await registerClientAgent({
202
+ villageAgentId: selectedAgent.villageAgent?.id || selectedAgent.villageAgentId || selectedAgent.id,
203
+ clientId: slug,
204
+ clientName: basics.name,
205
+ baseUrl: 'http://localhost:3000',
206
+ workflowType: workflowTypes[0],
207
+ });
208
+ agentConfig = { apiKey: regResult.apiKey, clientId: slug };
209
+ agentSpinner.succeed('Client agent registered!');
210
+ console.log(chalk.yellow.bold(' ⚠ Save your API key — it won\'t be shown again!'));
211
+ console.log(brand.teal(` API Key: ${brand.gold(regResult.apiKey)}`));
212
+ } else {
213
+ console.log(brand.teal(' No agents found. Create one first with: myvillage agent create'));
214
+ }
215
+ } catch (err) {
216
+ console.log(chalk.yellow(` ⚠ Failed to register client agent: ${err.response?.data?.error || err.message}`));
217
+ console.log(chalk.dim(' You can register later with: myvillage agent register-client'));
218
+ }
219
+ }
220
+ }
221
+
163
222
  // Scaffold project (always Next.js)
164
223
  const spinner = villageSpinner('Creating application...').start();
165
224
  try {
@@ -172,6 +231,7 @@ async function applicationFlow() {
172
231
  includeRestApi,
173
232
  mcpToolGroups,
174
233
  oauthCredentials,
234
+ agentConfig,
175
235
  });
176
236
 
177
237
  spinner.text = 'Installing dependencies...';
@@ -665,6 +665,72 @@ export async function gameSubmitCommand() {
665
665
  }
666
666
  }
667
667
 
668
+ // ── game draft (return to draft) ────────────────────
669
+
670
+ export async function gameDraftCommand() {
671
+ if (!isAuthenticated()) {
672
+ console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
673
+ return;
674
+ }
675
+
676
+ const gameId = getGameId();
677
+ if (!gameId) return;
678
+
679
+ const checkSpinner = villageSpinner('Checking game status...').start();
680
+ let game;
681
+ try {
682
+ const result = await getGameDetail(gameId);
683
+ game = result.game;
684
+ checkSpinner.stop();
685
+ } catch (err) {
686
+ const message = err.response?.data?.error || err.message;
687
+ checkSpinner.fail(`Failed to fetch game: ${message}`);
688
+ return;
689
+ }
690
+
691
+ if (game.status === 'DRAFT') {
692
+ console.log(brand.teal(`\n "${game.title}" is already in DRAFT status.\n`));
693
+ return;
694
+ }
695
+
696
+ if (game.status === 'PUBLISHED') {
697
+ console.log(chalk.red(`\n ✗ "${game.title}" is PUBLISHED and cannot be returned to draft.\n`));
698
+ return;
699
+ }
700
+
701
+ console.log();
702
+ console.log(brand.gold(` Return "${game.title}" to DRAFT?`));
703
+ console.log(brand.teal(` Current status: ${game.status}`));
704
+ console.log(brand.teal(' This will withdraw the game from review so you can make changes.'));
705
+ console.log();
706
+
707
+ const { confirm } = await inquirer.prompt([{
708
+ type: 'confirm',
709
+ name: 'confirm',
710
+ message: 'Return to draft?',
711
+ default: true,
712
+ }]);
713
+
714
+ if (!confirm) {
715
+ console.log(brand.teal('\n Cancelled.\n'));
716
+ return;
717
+ }
718
+
719
+ const spinner = villageSpinner('Updating status...').start();
720
+
721
+ try {
722
+ await updateGameMetadata(gameId, { status: 'DRAFT' });
723
+ spinner.succeed('Game returned to draft!');
724
+ console.log();
725
+ console.log(brand.green(` ✓ "${game.title}" is now in DRAFT status.`));
726
+ console.log(brand.teal(' You can now upload assets, update metadata, and redeploy.'));
727
+ console.log();
728
+ } catch (err) {
729
+ const message = err.response?.data?.error || err.message;
730
+ spinner.fail(`Failed to update status: ${message}`);
731
+ }
732
+ }
733
+
668
734
  // ── Helpers ───────────────────────────────────────────
669
735
 
670
736
  function getGameId() {