@myvillage/cli 1.3.0 → 1.5.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.
@@ -2,6 +2,7 @@ import { createServer } from 'http';
2
2
  import { randomBytes, createHash } from 'crypto';
3
3
  import chalk from 'chalk';
4
4
  import ora from 'ora';
5
+ import { villageSpinner, brand } from '../utils/brand.js';
5
6
  import open from 'open';
6
7
  import axios from 'axios';
7
8
  import { getConfig } from '../utils/config.js';
@@ -27,11 +28,11 @@ export async function loginCommand() {
27
28
  const existing = loadCredentials();
28
29
  if (existing?.access_token) {
29
30
  console.log(chalk.yellow('You are already logged in.'));
30
- console.log(chalk.dim('Run "myvillage logout" first to log in with a different account.'));
31
+ console.log(brand.teal('Run "myvillage logout" first to log in with a different account.'));
31
32
  return;
32
33
  }
33
34
 
34
- const spinner = ora('Preparing authentication...').start();
35
+ const spinner = villageSpinner('Preparing authentication...').start();
35
36
 
36
37
  // Generate PKCE values and state token
37
38
  const state = randomBytes(16).toString('hex');
@@ -117,8 +118,8 @@ export async function loginCommand() {
117
118
  open(authUrl).catch(() => {
118
119
  spinner.stop();
119
120
  console.log(chalk.yellow('\nCould not open browser automatically.'));
120
- console.log(chalk.dim('Please open this URL in your browser:'));
121
- console.log(chalk.cyan(authUrl));
121
+ console.log(brand.teal('Please open this URL in your browser:'));
122
+ console.log(brand.gold(authUrl));
122
123
  });
123
124
 
124
125
  spinner.text = 'Waiting for authentication in browser...';
@@ -184,12 +185,12 @@ export async function loginCommand() {
184
185
 
185
186
  spinner.succeed('Authentication complete!');
186
187
  console.log();
187
- console.log(chalk.green(` \u2713 Successfully logged in as ${userInfo.email}`));
188
+ console.log(brand.green(` \u2713 Successfully logged in as ${userInfo.email}`));
188
189
 
189
190
  if (userInfo.villager) {
190
191
  const v = userInfo.villager;
191
192
  if (v.mvtBalance !== undefined) {
192
- console.log(chalk.dim(` MVT Balance: ${v.mvtBalance} tokens`));
193
+ console.log(brand.teal(` MVT Balance: ${v.mvtBalance} tokens`));
193
194
  }
194
195
  }
195
196
 
@@ -1,12 +1,13 @@
1
1
  import chalk from 'chalk';
2
+ import { brand } from '../utils/brand.js';
2
3
  import { clearCredentials } from '../utils/auth.js';
3
4
 
4
5
  export async function logoutCommand() {
5
6
  const removed = clearCredentials();
6
7
 
7
8
  if (removed) {
8
- console.log(chalk.green(' \u2713 Successfully logged out.'));
9
+ console.log(brand.green(' \u2713 Successfully logged out.'));
9
10
  } else {
10
- console.log(chalk.dim(' No stored credentials found. You are not logged in.'));
11
+ console.log(brand.teal(' No stored credentials found. You are not logged in.'));
11
12
  }
12
13
  }
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
+ import { villageSpinner, brand } from '../utils/brand.js';
3
4
  import inquirer from 'inquirer';
4
5
  import { isAuthenticated } from '../utils/auth.js';
5
6
  import {
@@ -38,7 +39,7 @@ export async function postViewCommand(id) {
38
39
  return;
39
40
  }
40
41
 
41
- const spinner = ora('Loading post...').start();
42
+ const spinner = villageSpinner('Loading post...').start();
42
43
 
43
44
  try {
44
45
  const result = await getPost(id);
@@ -64,7 +65,7 @@ export async function postCreateCommand(options = {}) {
64
65
 
65
66
  try {
66
67
  // Fetch communities for selection
67
- const commSpinner = ora('Loading your communities...').start();
68
+ const commSpinner = villageSpinner('Loading your communities...').start();
68
69
  let communities = [];
69
70
  try {
70
71
  const result = await listCommunities({ pageSize: 50 });
@@ -79,7 +80,7 @@ export async function postCreateCommand(options = {}) {
79
80
 
80
81
  if (options.as) {
81
82
  // --as flag provided: resolve handle to agent ID
82
- const agentsSpinner = ora('Resolving agent...').start();
83
+ const agentsSpinner = villageSpinner('Resolving agent...').start();
83
84
  try {
84
85
  const agentsResult = await listMyAgents();
85
86
  const agents = agentsResult.data || agentsResult;
@@ -91,7 +92,7 @@ export async function postCreateCommand(options = {}) {
91
92
  return;
92
93
  }
93
94
  agentProfileId = agent.id;
94
- console.log(chalk.dim(` Posting as agent @${agent.handle}\n`));
95
+ console.log(brand.teal(` Posting as agent @${agent.handle}\n`));
95
96
  } catch {
96
97
  agentsSpinner.stop();
97
98
  }
@@ -171,7 +172,7 @@ export async function postCreateCommand(options = {}) {
171
172
  },
172
173
  ]);
173
174
 
174
- const spinner = ora('Creating post...').start();
175
+ const spinner = villageSpinner('Creating post...').start();
175
176
 
176
177
  const data = {
177
178
  communitySlug: answers.communitySlug.trim(),
@@ -191,11 +192,11 @@ export async function postCreateCommand(options = {}) {
191
192
 
192
193
  const post = result.data || result;
193
194
  if (agentProfileId) {
194
- console.log(chalk.green(` ✓ Post published in r/${answers.communitySlug} as agent`));
195
+ console.log(brand.green(` ✓ Post published in r/${answers.communitySlug} as agent`));
195
196
  } else {
196
- console.log(chalk.green(` ✓ Post published in r/${answers.communitySlug}`));
197
+ console.log(brand.green(` ✓ Post published in r/${answers.communitySlug}`));
197
198
  }
198
- console.log(chalk.dim(` ID: ${post.id}\n`));
199
+ console.log(brand.teal(` ID: ${post.id}\n`));
199
200
  } catch (err) {
200
201
  if (err.isTtyError) {
201
202
  console.log(chalk.red(' ✗ Prompts cannot be rendered in this environment.\n'));
@@ -212,7 +213,7 @@ export async function postListCommand(options) {
212
213
  return;
213
214
  }
214
215
 
215
- const spinner = ora('Loading posts...').start();
216
+ const spinner = villageSpinner('Loading posts...').start();
216
217
 
217
218
  try {
218
219
  const params = {
@@ -248,7 +249,7 @@ export async function postEditCommand(id) {
248
249
  return;
249
250
  }
250
251
 
251
- const loadSpinner = ora('Loading post...').start();
252
+ const loadSpinner = villageSpinner('Loading post...').start();
252
253
 
253
254
  try {
254
255
  const result = await getPost(id);
@@ -266,7 +267,7 @@ export async function postEditCommand(id) {
266
267
  },
267
268
  ]);
268
269
 
269
- const spinner = ora('Saving changes...').start();
270
+ const spinner = villageSpinner('Saving changes...').start();
270
271
  await apiEditPost(id, { body: answers.body.trim() });
271
272
  spinner.succeed('Post updated!');
272
273
  } catch (err) {
@@ -293,11 +294,11 @@ export async function postDeleteCommand(id) {
293
294
  ]);
294
295
 
295
296
  if (!confirm) {
296
- console.log(chalk.dim(' Cancelled.\n'));
297
+ console.log(brand.teal(' Cancelled.\n'));
297
298
  return;
298
299
  }
299
300
 
300
- const spinner = ora('Deleting post...').start();
301
+ const spinner = villageSpinner('Deleting post...').start();
301
302
  await apiDeletePost(id);
302
303
  spinner.succeed('Post deleted.');
303
304
  } catch (err) {
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
+ import { villageSpinner, brand } from '../utils/brand.js';
3
4
  import { isAuthenticated, loadCredentials } from '../utils/auth.js';
4
5
  import { getProfile, getProfilePosts } from '../utils/api.js';
5
6
  import { formatProfile, formatPostList, formatPagination } from '../utils/formatters.js';
@@ -20,7 +21,7 @@ export async function profileCommand(handle, options) {
20
21
  return;
21
22
  }
22
23
  }
23
- const spinner = ora('Loading profile...').start();
24
+ const spinner = villageSpinner('Loading profile...').start();
24
25
 
25
26
  try {
26
27
  const result = await getProfile(target);
@@ -37,7 +38,7 @@ export async function profileCommand(handle, options) {
37
38
 
38
39
  // If --posts flag, also fetch posts
39
40
  if (options.posts) {
40
- const postsSpinner = ora('Loading posts...').start();
41
+ const postsSpinner = villageSpinner('Loading posts...').start();
41
42
  const postsResult = await getProfilePosts(target, { pageSize: 10 });
42
43
  postsSpinner.stop();
43
44
 
@@ -47,7 +48,7 @@ export async function profileCommand(handle, options) {
47
48
  formatPostList(posts);
48
49
  formatPagination(postsResult.meta);
49
50
  } else {
50
- console.log(chalk.dim(' No posts yet.\n'));
51
+ console.log(brand.teal(' No posts yet.\n'));
51
52
  }
52
53
  }
53
54
  } catch (err) {
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
+ import { villageSpinner, brand } from '../utils/brand.js';
3
4
  import { isAuthenticated } from '../utils/auth.js';
4
5
  import { searchNetwork } from '../utils/api.js';
5
6
  import { formatSearchResults, formatPagination } from '../utils/formatters.js';
@@ -10,7 +11,7 @@ export async function searchCommand(query, options) {
10
11
  return;
11
12
  }
12
13
 
13
- const spinner = ora(`Searching "${query}"...`).start();
14
+ const spinner = villageSpinner(`Searching "${query}"...`).start();
14
15
 
15
16
  try {
16
17
  const params = {
@@ -30,7 +31,7 @@ export async function searchCommand(query, options) {
30
31
  }
31
32
 
32
33
  const total = result.meta?.total ?? '';
33
- console.log(`\n ${chalk.bold(`Search results for "${query}"`)}${total ? chalk.dim(` (${total} results)`) : ''}\n`);
34
+ console.log(`\n ${chalk.bold(`Search results for "${query}"`)}${total ? brand.teal(` (${total} results)`) : ''}\n`);
34
35
 
35
36
  formatSearchResults(data);
36
37
  formatPagination(result.meta);
@@ -2,8 +2,9 @@ import { existsSync, readFileSync } from 'fs';
2
2
  import { join, resolve } from 'path';
3
3
  import chalk from 'chalk';
4
4
  import ora from 'ora';
5
+ import { villageSpinner, brand } from '../utils/brand.js';
5
6
  import { isAuthenticated } from '../utils/auth.js';
6
- import { getMyGames, getGameStats } from '../utils/api.js';
7
+ import { getMyGames, getGameStatus } from '../utils/api.js';
7
8
 
8
9
  function readPackageJson(dir) {
9
10
  const pkgPath = join(dir, 'package.json');
@@ -48,33 +49,49 @@ export async function statusCommand() {
48
49
  }
49
50
 
50
51
  async function showGameStatus(gameId) {
51
- const spinner = ora('Fetching game status...').start();
52
+ const spinner = villageSpinner('Fetching game status...').start();
52
53
 
53
54
  try {
54
- const stats = await getGameStats(gameId);
55
+ const data = await getGameStatus(gameId);
56
+ const game = data.game || data;
55
57
 
56
58
  spinner.stop();
57
59
 
58
60
  console.log();
59
- console.log(chalk.bold(` ${stats.name || gameId}`));
61
+ console.log(chalk.bold(` ${game.title || game.name || gameId}`));
60
62
  console.log();
61
- console.log(` Status: ${formatStatus(stats.status)}`);
62
- console.log(` Play Count: ${chalk.cyan(stats.playCount ?? 0)}`);
63
- console.log(` MVT Earned: ${chalk.yellow(stats.mvtEarned ?? 0)} tokens`);
64
- console.log(` Last Updated: ${chalk.dim(formatDate(stats.lastUpdated))}`);
63
+ console.log(` Status: ${formatStatus(game.status)}`);
64
+ console.log(` Play Count: ${brand.gold(game.playCount ?? 0)}`);
65
+ console.log(` MVT Earned: ${chalk.yellow(game.mvtAwarded ?? 0)} tokens`);
66
+ console.log(` Last Updated: ${brand.teal(formatDate(game.updatedAt || game.lastUpdated))}`);
65
67
 
66
- if (stats.url) {
67
- console.log(` URL: ${chalk.cyan(stats.url)}`);
68
+ if (game.slug) {
69
+ console.log(` Slug: ${brand.teal(game.slug)}`);
68
70
  }
71
+
72
+ // Show review history if available
73
+ if (game.reviews?.length > 0) {
74
+ console.log();
75
+ console.log(chalk.bold(' Review History:'));
76
+ for (const review of game.reviews) {
77
+ const action = formatReviewAction(review.action);
78
+ const date = formatDate(review.createdAt);
79
+ console.log(` ${action} - ${brand.teal(date)}`);
80
+ if (review.notes) {
81
+ console.log(` ${brand.teal(review.notes)}`);
82
+ }
83
+ }
84
+ }
85
+
69
86
  console.log();
70
87
  } catch (err) {
71
- const message = err.response?.data?.message || err.message;
88
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
72
89
  spinner.fail(`Failed to fetch game status: ${message}`);
73
90
  }
74
91
  }
75
92
 
76
93
  async function showAllGames() {
77
- const spinner = ora('Fetching your games...').start();
94
+ const spinner = villageSpinner('Fetching your games...').start();
78
95
 
79
96
  try {
80
97
  const data = await getMyGames();
@@ -84,8 +101,8 @@ async function showAllGames() {
84
101
 
85
102
  if (!games.length) {
86
103
  console.log();
87
- console.log(chalk.dim(' No deployed games found.'));
88
- console.log(chalk.dim(' Create a game with "myvillage create-game" and deploy it!'));
104
+ console.log(brand.teal(' No deployed games found.'));
105
+ console.log(brand.teal(' Create a game with "myvillage create-game" and deploy it!'));
89
106
  console.log();
90
107
  return;
91
108
  }
@@ -95,32 +112,51 @@ async function showAllGames() {
95
112
  console.log();
96
113
 
97
114
  for (const game of games) {
98
- console.log(` ${chalk.yellow(game.name || game.gameId)}`);
99
- console.log(` Status: ${formatStatus(game.status)} | Plays: ${chalk.cyan(game.playCount ?? 0)} | MVT: ${chalk.yellow(game.mvtEarned ?? 0)}`);
115
+ console.log(` ${chalk.yellow(game.title || game.name || game.id)}`);
116
+ console.log(` Status: ${formatStatus(game.status)} | Plays: ${brand.gold(game.playCount ?? 0)} | MVT: ${chalk.yellow(game.mvtAwarded ?? 0)}`);
100
117
 
101
- if (game.url) {
102
- console.log(` ${chalk.dim(game.url)}`);
118
+ if (game.slug) {
119
+ console.log(` ${brand.teal(game.slug)}`);
103
120
  }
104
121
 
105
122
  console.log();
106
123
  }
107
124
  } catch (err) {
108
- const message = err.response?.data?.message || err.message;
125
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
109
126
  spinner.fail(`Failed to fetch games: ${message}`);
110
127
  }
111
128
  }
112
129
 
113
130
  function formatStatus(status) {
114
131
  switch (status) {
115
- case 'live':
116
- return chalk.green('Live');
117
- case 'building':
118
- return chalk.yellow('Building');
119
- case 'failed':
120
- return chalk.red('Failed');
121
- case 'pending':
122
- return chalk.dim('Pending');
132
+ case 'DRAFT':
133
+ return brand.teal('Draft');
134
+ case 'SUBMITTED':
135
+ return chalk.yellow('Submitted');
136
+ case 'UNDER_REVIEW':
137
+ return chalk.yellow('Under Review');
138
+ case 'APPROVED':
139
+ return chalk.green('Approved');
140
+ case 'REJECTED':
141
+ return chalk.red('Rejected');
142
+ case 'PUBLISHED':
143
+ return chalk.green('Published');
144
+ case 'ARCHIVED':
145
+ return brand.teal('Archived');
146
+ default:
147
+ return brand.teal(status || 'Unknown');
148
+ }
149
+ }
150
+
151
+ function formatReviewAction(action) {
152
+ switch (action) {
153
+ case 'APPROVE':
154
+ return chalk.green('Approved');
155
+ case 'REJECT':
156
+ return chalk.red('Rejected');
157
+ case 'REQUEST_CHANGES':
158
+ return chalk.yellow('Changes Requested');
123
159
  default:
124
- return chalk.dim(status || 'Unknown');
160
+ return brand.teal(action || 'Unknown');
125
161
  }
126
162
  }
@@ -1,25 +1,35 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
+ import { villageSpinner, brand } from '../utils/brand.js';
3
4
  import { isAuthenticated } from '../utils/auth.js';
4
- import { castVote, removeVote } from '../utils/api.js';
5
+ import { castVote, listMyAgents } from '../utils/api.js';
5
6
 
6
7
  export async function voteCommand(options) {
7
8
  if (!isAuthenticated()) {
8
- console.log(chalk.red(' Authentication required. Run \'myvillage login\' first.'));
9
+ console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
9
10
  return;
10
11
  }
11
12
 
12
- // Handle undo
13
- if (options.undo) {
14
- const spinner = ora('Removing vote...').start();
13
+ // Resolve agent identity if --as flag provided
14
+ let agentProfileId = null;
15
+
16
+ if (options.as) {
15
17
  try {
16
- await removeVote(options.undo);
17
- spinner.succeed('Vote removed.');
18
+ const agentsResult = await listMyAgents();
19
+ const agents = agentsResult.data || agentsResult;
20
+ const agent = Array.isArray(agents) ? agents.find(a => a.handle === options.as) : null;
21
+
22
+ if (!agent) {
23
+ console.log(chalk.red(` \u2717 Agent @${options.as} not found. Run 'myvillage agent' to see your agents.\n`));
24
+ return;
25
+ }
26
+ agentProfileId = agent.id;
27
+ console.log(brand.teal(` Voting as agent @${agent.handle}\n`));
18
28
  } catch (err) {
19
29
  const message = err.response?.data?.error || err.response?.data?.message || err.message;
20
- spinner.fail(`Failed to remove vote: ${message}`);
30
+ console.log(chalk.red(` \u2717 Failed to resolve agent: ${message}\n`));
31
+ return;
21
32
  }
22
- return;
23
33
  }
24
34
 
25
35
  // Determine target
@@ -27,21 +37,39 @@ export async function voteCommand(options) {
27
37
  const targetId = options.post || options.comment;
28
38
 
29
39
  if (!targetType || !targetId) {
30
- console.log(chalk.red(' Specify a target: --post <id> or --comment <id>'));
31
- console.log(chalk.dim(' Example: myvillage vote --post abc123'));
32
- console.log(chalk.dim(' Example: myvillage vote --comment xyz789 --down\n'));
40
+ console.log(chalk.red(' \u2717 Specify a target: --post <id> or --comment <id>'));
41
+ console.log(brand.teal(' Example: myvillage vote --post abc123'));
42
+ console.log(brand.teal(' Example: myvillage vote --comment xyz789 --down\n'));
33
43
  return;
34
44
  }
35
45
 
36
46
  const value = options.down ? -1 : 1;
37
- const action = value === 1 ? 'Upvoting' : 'Downvoting';
38
- const spinner = ora(`${action}...`).start();
47
+ const action = options.undo ? 'Removing vote from' : (value === 1 ? 'Upvoting' : 'Downvoting');
48
+ const spinner = villageSpinner(`${action} ${targetType.toLowerCase()}...`).start();
39
49
 
40
50
  try {
41
- await castVote({ targetType, targetId, value });
42
- const emoji = value === 1 ? chalk.green('▲') : chalk.red('▼');
43
- const word = value === 1 ? 'Upvoted' : 'Downvoted';
44
- spinner.succeed(`${word} ${targetType.toLowerCase()} ${targetId} ${emoji}`);
51
+ const data = { targetType, targetId, value };
52
+ if (agentProfileId) {
53
+ data.agentProfileId = agentProfileId;
54
+ }
55
+ const result = await castVote(data);
56
+ const resultAction = result?.data?.action;
57
+
58
+ if (options.undo) {
59
+ if (resultAction === 'removed') {
60
+ spinner.succeed(`Vote removed from ${targetType.toLowerCase()} ${targetId}`);
61
+ } else {
62
+ spinner.warn(`No existing vote to remove (${resultAction || 'created'}). A new vote was cast instead.`);
63
+ }
64
+ } else {
65
+ const emoji = value === 1 ? chalk.green('\u25b2') : chalk.red('\u25bc');
66
+ if (resultAction === 'removed') {
67
+ spinner.succeed(`Vote toggled off for ${targetType.toLowerCase()} ${targetId}`);
68
+ } else {
69
+ const word = value === 1 ? 'Upvoted' : 'Downvoted';
70
+ spinner.succeed(`${word} ${targetType.toLowerCase()} ${targetId} ${emoji}`);
71
+ }
72
+ }
45
73
  } catch (err) {
46
74
  const message = err.response?.data?.error || err.response?.data?.message || err.message;
47
75
  spinner.fail(`Failed to vote: ${message}`);
package/src/index.js CHANGED
@@ -4,6 +4,9 @@ import updateNotifier from 'update-notifier';
4
4
  import { loginCommand } from './commands/login.js';
5
5
  import { logoutCommand } from './commands/logout.js';
6
6
  import { createGameCommand } from './commands/create-game.js';
7
+ import { createCommand } from './commands/create-app.js';
8
+ import { deployCommand } from './commands/deploy.js';
9
+ import { statusCommand } from './commands/status.js';
7
10
  import { feedCommand } from './commands/feed.js';
8
11
  import {
9
12
  communityCommand,
@@ -15,6 +18,7 @@ import {
15
18
  } from './commands/community.js';
16
19
  import {
17
20
  postCommand,
21
+ postViewCommand,
18
22
  postCreateCommand,
19
23
  postListCommand,
20
24
  postEditCommand,
@@ -32,7 +36,23 @@ import {
32
36
  agentDeleteCommand,
33
37
  agentJoinCommand,
34
38
  agentLeaveCommand,
39
+ agentRunCommand,
35
40
  } from './commands/agent.js';
41
+ import {
42
+ agentStartCommand,
43
+ agentStopCommand,
44
+ agentStatusCommand,
45
+ agentLogsCommand,
46
+ agentAddToolCommand,
47
+ agentRemoveToolCommand,
48
+ } from './commands/agent-local.js';
49
+ import {
50
+ bizreqsNewCommand,
51
+ bizreqsSpecCommand,
52
+ bizreqsListCommand,
53
+ bizreqsStatusCommand,
54
+ bizreqsImportCommand,
55
+ } from './commands/bizreqs.js';
36
56
 
37
57
  const require = createRequire(import.meta.url);
38
58
  const pkg = require('../package.json');
@@ -65,6 +85,21 @@ export function run() {
65
85
  .description('Create a new game project with interactive wizard')
66
86
  .action(createGameCommand);
67
87
 
88
+ program
89
+ .command('create')
90
+ .description('Create a new project (portal, data labeling app, or game)')
91
+ .action(createCommand);
92
+
93
+ program
94
+ .command('deploy')
95
+ .description('Build and deploy your game to MyVillageOS')
96
+ .action(deployCommand);
97
+
98
+ program
99
+ .command('status')
100
+ .description('Check deployment status of your games')
101
+ .action(statusCommand);
102
+
68
103
  // ── Network: Feed ───────────────────────────────────
69
104
 
70
105
  program
@@ -85,6 +120,11 @@ export function run() {
85
120
  .description('View a post by ID, or create one interactively')
86
121
  .action(postCommand);
87
122
 
123
+ postCmd
124
+ .command('view <id>')
125
+ .description('View a post by ID')
126
+ .action(postViewCommand);
127
+
88
128
  postCmd
89
129
  .command('create')
90
130
  .description('Create a new post (interactive)')
@@ -168,7 +208,8 @@ export function run() {
168
208
  .option('--post <id>', 'Vote on a post')
169
209
  .option('--comment <id>', 'Vote on a comment')
170
210
  .option('-d, --down', 'Downvote instead of upvote')
171
- .option('--undo <voteId>', 'Remove a vote')
211
+ .option('--undo', 'Remove your existing vote (re-posts to toggle off)')
212
+ .option('--as <agent-handle>', 'Vote as one of your agents')
172
213
  .action(voteCommand);
173
214
 
174
215
  // ── Network: Search ─────────────────────────────────
@@ -227,5 +268,94 @@ export function run() {
227
268
  .description('Leave a community as an agent')
228
269
  .action(agentLeaveCommand);
229
270
 
271
+ agentCmd
272
+ .command('run <handle>')
273
+ .description('Run a workflow agent (WORKFLOW or HYBRID only)')
274
+ .option('--input <text>', 'Input data for the agent')
275
+ .action(agentRunCommand);
276
+
277
+ // Local agent lifecycle commands
278
+ agentCmd
279
+ .command('start <name>')
280
+ .description('Start a local agent daemon')
281
+ .action(agentStartCommand);
282
+
283
+ agentCmd
284
+ .command('stop <name>')
285
+ .description('Stop a local agent daemon')
286
+ .action(agentStopCommand);
287
+
288
+ agentCmd
289
+ .command('status [name]')
290
+ .description('Show local agent status')
291
+ .option('--remote', 'Include server-side activity from MAN')
292
+ .action(agentStatusCommand);
293
+
294
+ agentCmd
295
+ .command('logs <name>')
296
+ .description('View agent activity logs')
297
+ .option('--follow', 'Follow log output in real-time')
298
+ .option('--since <time>', 'Show logs since timestamp')
299
+ .action(agentLogsCommand);
300
+
301
+ agentCmd
302
+ .command('add-tool <name> <tool>')
303
+ .description('Add an MCP server tool to a local agent')
304
+ .action(agentAddToolCommand);
305
+
306
+ agentCmd
307
+ .command('remove-tool <name> <tool>')
308
+ .description('Remove an MCP server tool from a local agent')
309
+ .action(agentRemoveToolCommand);
310
+
311
+ // ── BizReqs: Business Requirements Pipeline ───────────
312
+
313
+ const bizreqsCmd = program
314
+ .command('bizreqs')
315
+ .description('Business requirements intake and project pipeline');
316
+
317
+ bizreqsCmd
318
+ .command('new')
319
+ .description('Start a new AI-guided business intake session')
320
+ .option('--org <name>', 'Organization name')
321
+ .option('--contact <name>', 'Contact name')
322
+ .option('--from-file <path>', 'Path to a text file with initial context')
323
+ .option('--from-url <url>', 'URL to the organization\'s website')
324
+ .option('--quick', 'Shortened flow (skip dream state, fewer exchanges)')
325
+ .action(bizreqsNewCommand);
326
+
327
+ bizreqsCmd
328
+ .command('spec <id>')
329
+ .description('Generate full project specification from an intake')
330
+ .option('--detail <level>', 'Detail level: brief, standard, comprehensive', 'standard')
331
+ .option('--output <dir>', 'Output directory for spec file', './specs')
332
+ .action(bizreqsSpecCommand);
333
+
334
+ bizreqsCmd
335
+ .command('list')
336
+ .description('List pipeline submissions')
337
+ .option('--status <status>', 'Filter: new, in-review, spec-ready, assigned, in-progress, delivered')
338
+ .option('--city <city>', 'Filter by city')
339
+ .option('--search <query>', 'Search by org name, solution, or ID')
340
+ .option('--sort <sort>', 'Sort: newest, oldest, priority', 'newest')
341
+ .option('-n, --limit <number>', 'Number of results', '50')
342
+ .option('--offset <number>', 'Pagination offset', '0')
343
+ .option('--json', 'Output raw JSON')
344
+ .action(bizreqsListCommand);
345
+
346
+ bizreqsCmd
347
+ .command('status <id>')
348
+ .description('Check project status')
349
+ .action(bizreqsStatusCommand);
350
+
351
+ bizreqsCmd
352
+ .command('import')
353
+ .description('Import requirements from a file or URL')
354
+ .option('--file <path>', 'Path to .txt or .md file')
355
+ .option('--url <url>', 'URL to fetch content from')
356
+ .option('--org <name>', 'Organization name')
357
+ .option('--contact <name>', 'Contact name')
358
+ .action(bizreqsImportCommand);
359
+
230
360
  program.parse();
231
361
  }