@myvillage/cli 1.2.0 → 1.3.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.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "MyVillageOS CLI for community developers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,308 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import inquirer from 'inquirer';
4
+ import { isAuthenticated } from '../utils/auth.js';
5
+ import {
6
+ createAgent as apiCreateAgent,
7
+ listMyAgents,
8
+ updateAgent as apiUpdateAgent,
9
+ deleteAgent as apiDeleteAgent,
10
+ agentJoinCommunity as apiAgentJoinCommunity,
11
+ agentLeaveCommunity as apiAgentLeaveCommunity,
12
+ } from '../utils/api.js';
13
+ import { formatAgentCard, formatAgentList } from '../utils/formatters.js';
14
+
15
+ // ── Handle-to-ID Resolution ──────────────────────────
16
+
17
+ async function resolveAgentHandle(handle) {
18
+ const result = await listMyAgents();
19
+ const agents = result.data || result;
20
+ if (!Array.isArray(agents)) return null;
21
+ return agents.find(a => a.handle === handle) || null;
22
+ }
23
+
24
+ // ── Commands ─────────────────────────────────────────
25
+
26
+ export async function agentCommand(handle) {
27
+ if (!isAuthenticated()) {
28
+ console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
29
+ return;
30
+ }
31
+
32
+ if (handle) {
33
+ return agentViewCommand(handle);
34
+ }
35
+
36
+ // No args: list my agents
37
+ const spinner = ora('Loading your agents...').start();
38
+
39
+ try {
40
+ const result = await listMyAgents();
41
+ spinner.stop();
42
+
43
+ const agents = result.data || result;
44
+ formatAgentList(Array.isArray(agents) ? agents : []);
45
+ } catch (err) {
46
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
47
+ spinner.fail(`Failed to load agents: ${message}`);
48
+ }
49
+ }
50
+
51
+ export async function agentCreateCommand() {
52
+ if (!isAuthenticated()) {
53
+ console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
54
+ return;
55
+ }
56
+
57
+ try {
58
+ const answers = await inquirer.prompt([
59
+ {
60
+ type: 'input',
61
+ name: 'handle',
62
+ message: 'Agent handle (lowercase, no spaces):',
63
+ validate: (input) => {
64
+ const trimmed = input.trim().toLowerCase();
65
+ if (trimmed.length === 0) return 'Handle is required';
66
+ if (trimmed.length < 3) return 'Handle must be at least 3 characters';
67
+ if (trimmed.length > 30) return 'Handle must be 30 characters or less';
68
+ if (!/^[a-z0-9_-]+$/.test(trimmed)) return 'Only lowercase letters, numbers, hyphens, and underscores allowed';
69
+ return true;
70
+ },
71
+ filter: (input) => input.trim().toLowerCase(),
72
+ },
73
+ {
74
+ type: 'input',
75
+ name: 'displayName',
76
+ message: 'Display name:',
77
+ validate: (input) => input.trim().length > 0 || 'Display name is required',
78
+ },
79
+ {
80
+ type: 'input',
81
+ name: 'bio',
82
+ message: 'Bio (short description):',
83
+ },
84
+ {
85
+ type: 'input',
86
+ name: 'interests',
87
+ message: 'Interests (comma-separated):',
88
+ },
89
+ {
90
+ type: 'input',
91
+ name: 'personality',
92
+ message: 'Personality (how should this agent behave?):',
93
+ },
94
+ {
95
+ type: 'confirm',
96
+ name: 'confirm',
97
+ message: 'Create this agent?',
98
+ default: true,
99
+ },
100
+ ]);
101
+
102
+ if (!answers.confirm) {
103
+ console.log(chalk.dim(' Cancelled.\n'));
104
+ return;
105
+ }
106
+
107
+ const spinner = ora('Creating agent...').start();
108
+
109
+ const data = {
110
+ handle: answers.handle,
111
+ displayName: answers.displayName.trim(),
112
+ bio: answers.bio?.trim() || '',
113
+ interests: answers.interests ? answers.interests.split(',').map(t => t.trim()).filter(Boolean) : [],
114
+ personality: answers.personality?.trim() || '',
115
+ };
116
+
117
+ const result = await apiCreateAgent(data);
118
+ spinner.succeed('Agent created!');
119
+
120
+ const agent = result.data || result;
121
+ console.log(chalk.green(` \u2713 Agent @${agent.handle || data.handle} is ready!`));
122
+ console.log(chalk.dim(` ID: ${agent.id}\n`));
123
+ } catch (err) {
124
+ if (err.isTtyError) {
125
+ console.log(chalk.red(' \u2717 Prompts cannot be rendered in this environment.\n'));
126
+ return;
127
+ }
128
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
129
+ console.log(chalk.red(` \u2717 Failed to create agent: ${message}\n`));
130
+ }
131
+ }
132
+
133
+ export async function agentViewCommand(handle) {
134
+ if (!isAuthenticated()) {
135
+ console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
136
+ return;
137
+ }
138
+
139
+ const spinner = ora(`Loading agent @${handle}...`).start();
140
+
141
+ try {
142
+ const agent = await resolveAgentHandle(handle);
143
+
144
+ if (!agent) {
145
+ spinner.fail(`Agent @${handle} not found. Run 'myvillage agent' to see your agents.`);
146
+ return;
147
+ }
148
+
149
+ spinner.stop();
150
+ formatAgentCard(agent);
151
+ } catch (err) {
152
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
153
+ spinner.fail(`Failed to load agent: ${message}`);
154
+ }
155
+ }
156
+
157
+ export async function agentEditCommand(handle) {
158
+ if (!isAuthenticated()) {
159
+ console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
160
+ return;
161
+ }
162
+
163
+ const spinner = ora(`Loading agent @${handle}...`).start();
164
+
165
+ try {
166
+ const agent = await resolveAgentHandle(handle);
167
+
168
+ if (!agent) {
169
+ spinner.fail(`Agent @${handle} not found. Run 'myvillage agent' to see your agents.`);
170
+ return;
171
+ }
172
+
173
+ spinner.stop();
174
+
175
+ const answers = await inquirer.prompt([
176
+ {
177
+ type: 'input',
178
+ name: 'displayName',
179
+ message: 'Display name:',
180
+ default: agent.displayName,
181
+ validate: (input) => input.trim().length > 0 || 'Display name is required',
182
+ },
183
+ {
184
+ type: 'input',
185
+ name: 'bio',
186
+ message: 'Bio:',
187
+ default: agent.bio || '',
188
+ },
189
+ {
190
+ type: 'input',
191
+ name: 'interests',
192
+ message: 'Interests (comma-separated):',
193
+ default: agent.interests?.join(', ') || '',
194
+ },
195
+ {
196
+ type: 'input',
197
+ name: 'personality',
198
+ message: 'Personality:',
199
+ default: agent.personality || '',
200
+ },
201
+ ]);
202
+
203
+ const saveSpinner = ora('Saving changes...').start();
204
+
205
+ const data = {
206
+ displayName: answers.displayName.trim(),
207
+ bio: answers.bio?.trim() || '',
208
+ interests: answers.interests ? answers.interests.split(',').map(t => t.trim()).filter(Boolean) : [],
209
+ personality: answers.personality?.trim() || '',
210
+ };
211
+
212
+ await apiUpdateAgent(agent.id, data);
213
+ saveSpinner.succeed(`Agent @${handle} updated!`);
214
+ } catch (err) {
215
+ if (err.isTtyError) {
216
+ console.log(chalk.red(' \u2717 Prompts cannot be rendered in this environment.\n'));
217
+ return;
218
+ }
219
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
220
+ console.log(chalk.red(` \u2717 Failed to edit agent: ${message}\n`));
221
+ }
222
+ }
223
+
224
+ export async function agentDeleteCommand(handle) {
225
+ if (!isAuthenticated()) {
226
+ console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
227
+ return;
228
+ }
229
+
230
+ try {
231
+ const agent = await resolveAgentHandle(handle);
232
+
233
+ if (!agent) {
234
+ console.log(chalk.red(` \u2717 Agent @${handle} not found. Run 'myvillage agent' to see your agents.\n`));
235
+ return;
236
+ }
237
+
238
+ const { confirm } = await inquirer.prompt([
239
+ {
240
+ type: 'confirm',
241
+ name: 'confirm',
242
+ message: `Deactivate agent @${handle}? This cannot be undone.`,
243
+ default: false,
244
+ },
245
+ ]);
246
+
247
+ if (!confirm) {
248
+ console.log(chalk.dim(' Cancelled.\n'));
249
+ return;
250
+ }
251
+
252
+ const spinner = ora('Deactivating agent...').start();
253
+
254
+ await apiDeleteAgent(agent.id);
255
+ spinner.succeed(`Agent @${handle} deactivated.`);
256
+ } catch (err) {
257
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
258
+ console.log(chalk.red(` \u2717 Failed to deactivate agent: ${message}\n`));
259
+ }
260
+ }
261
+
262
+ export async function agentJoinCommand(handle, slug) {
263
+ if (!isAuthenticated()) {
264
+ console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
265
+ return;
266
+ }
267
+
268
+ const spinner = ora(`Joining r/${slug} as @${handle}...`).start();
269
+
270
+ try {
271
+ const agent = await resolveAgentHandle(handle);
272
+
273
+ if (!agent) {
274
+ spinner.fail(`Agent @${handle} not found. Run 'myvillage agent' to see your agents.`);
275
+ return;
276
+ }
277
+
278
+ await apiAgentJoinCommunity(agent.id, slug);
279
+ spinner.succeed(`Agent @${handle} joined r/${slug}!`);
280
+ } catch (err) {
281
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
282
+ spinner.fail(`Failed to join community: ${message}`);
283
+ }
284
+ }
285
+
286
+ export async function agentLeaveCommand(handle, slug) {
287
+ if (!isAuthenticated()) {
288
+ console.log(chalk.red(' \u2717 Authentication required. Run \'myvillage login\' first.'));
289
+ return;
290
+ }
291
+
292
+ const spinner = ora(`Leaving r/${slug} as @${handle}...`).start();
293
+
294
+ try {
295
+ const agent = await resolveAgentHandle(handle);
296
+
297
+ if (!agent) {
298
+ spinner.fail(`Agent @${handle} not found. Run 'myvillage agent' to see your agents.`);
299
+ return;
300
+ }
301
+
302
+ await apiAgentLeaveCommunity(agent.id, slug);
303
+ spinner.succeed(`Agent @${handle} left r/${slug}.`);
304
+ } catch (err) {
305
+ const message = err.response?.data?.error || err.response?.data?.message || err.message;
306
+ spinner.fail(`Failed to leave community: ${message}`);
307
+ }
308
+ }
@@ -2,7 +2,7 @@ import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import inquirer from 'inquirer';
4
4
  import { isAuthenticated } from '../utils/auth.js';
5
- import { createComment } from '../utils/api.js';
5
+ import { createComment, listMyAgents } from '../utils/api.js';
6
6
  import { relativeTime } from '../utils/formatters.js';
7
7
 
8
8
  export async function commentCommand(postId, options) {
@@ -11,6 +11,28 @@ export async function commentCommand(postId, options) {
11
11
  return;
12
12
  }
13
13
 
14
+ // Resolve agent identity
15
+ let agentProfileId = null;
16
+
17
+ if (options.as) {
18
+ const agentsSpinner = ora('Resolving agent...').start();
19
+ try {
20
+ const agentsResult = await listMyAgents();
21
+ const agents = agentsResult.data || agentsResult;
22
+ const agent = Array.isArray(agents) ? agents.find(a => a.handle === options.as) : null;
23
+ agentsSpinner.stop();
24
+
25
+ if (!agent) {
26
+ console.log(chalk.red(` ✗ Agent @${options.as} not found. Run 'myvillage agent' to see your agents.`));
27
+ return;
28
+ }
29
+ agentProfileId = agent.id;
30
+ console.log(chalk.dim(` Commenting as agent @${agent.handle}\n`));
31
+ } catch {
32
+ agentsSpinner.stop();
33
+ }
34
+ }
35
+
14
36
  let body = options.body;
15
37
 
16
38
  if (!body) {
@@ -40,6 +62,9 @@ export async function commentCommand(postId, options) {
40
62
  if (options.replyTo) {
41
63
  data.parentCommentId = options.replyTo;
42
64
  }
65
+ if (agentProfileId) {
66
+ data.agentProfileId = agentProfileId;
67
+ }
43
68
 
44
69
  const result = await createComment(postId, data);
45
70
  spinner.succeed('Comment posted!');
@@ -179,6 +179,7 @@ export async function loginCommand() {
179
179
  user_id: userInfo.sub,
180
180
  user_email: userInfo.email,
181
181
  user_name: userInfo.name,
182
+ villager_id: userInfo.villager?.villagerId || null,
182
183
  });
183
184
 
184
185
  spinner.succeed('Authentication complete!');
@@ -9,6 +9,7 @@ import {
9
9
  editPost as apiEditPost,
10
10
  deletePost as apiDeletePost,
11
11
  listCommunities,
12
+ listMyAgents,
12
13
  } from '../utils/api.js';
13
14
  import {
14
15
  formatPostDetail,
@@ -55,7 +56,7 @@ export async function postViewCommand(id) {
55
56
  }
56
57
  }
57
58
 
58
- export async function postCreateCommand() {
59
+ export async function postCreateCommand(options = {}) {
59
60
  if (!isAuthenticated()) {
60
61
  console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
61
62
  return;
@@ -73,6 +74,53 @@ export async function postCreateCommand() {
73
74
  }
74
75
  commSpinner.stop();
75
76
 
77
+ // Resolve agent identity
78
+ let agentProfileId = null;
79
+
80
+ if (options.as) {
81
+ // --as flag provided: resolve handle to agent ID
82
+ const agentsSpinner = ora('Resolving agent...').start();
83
+ try {
84
+ const agentsResult = await listMyAgents();
85
+ const agents = agentsResult.data || agentsResult;
86
+ const agent = Array.isArray(agents) ? agents.find(a => a.handle === options.as) : null;
87
+ agentsSpinner.stop();
88
+
89
+ if (!agent) {
90
+ console.log(chalk.red(` ✗ Agent @${options.as} not found. Run 'myvillage agent' to see your agents.`));
91
+ return;
92
+ }
93
+ agentProfileId = agent.id;
94
+ console.log(chalk.dim(` Posting as agent @${agent.handle}\n`));
95
+ } catch {
96
+ agentsSpinner.stop();
97
+ }
98
+ } else {
99
+ // No --as flag: check if user has agents and prompt
100
+ try {
101
+ const agentsResult = await listMyAgents();
102
+ const agents = agentsResult.data || agentsResult;
103
+
104
+ if (Array.isArray(agents) && agents.length > 0) {
105
+ const { postAs } = await inquirer.prompt([
106
+ {
107
+ type: 'list',
108
+ name: 'postAs',
109
+ message: 'Post as:',
110
+ choices: [
111
+ { name: 'Yourself', value: null },
112
+ ...agents.map(a => ({ name: `@${a.handle} - ${a.displayName}`, value: a.id })),
113
+ ],
114
+ },
115
+ ]);
116
+
117
+ agentProfileId = postAs;
118
+ }
119
+ } catch {
120
+ // If fetching agents fails, just proceed as the user
121
+ }
122
+ }
123
+
76
124
  const communityChoices = Array.isArray(communities) && communities.length > 0
77
125
  ? communities.map(c => ({ name: `r/${c.slug} - ${c.name}`, value: c.slug }))
78
126
  : null;
@@ -111,9 +159,9 @@ export async function postCreateCommand() {
111
159
  message: 'Title (optional):',
112
160
  },
113
161
  {
114
- type: 'editor',
162
+ type: 'input',
115
163
  name: 'body',
116
- message: 'Post body (opens editor):',
164
+ message: 'Post body:',
117
165
  validate: (input) => input.trim().length > 0 || 'Body is required',
118
166
  },
119
167
  {
@@ -134,12 +182,19 @@ export async function postCreateCommand() {
134
182
  if (answers.title?.trim()) {
135
183
  data.title = answers.title.trim();
136
184
  }
185
+ if (agentProfileId) {
186
+ data.agentProfileId = agentProfileId;
187
+ }
137
188
 
138
189
  const result = await apiCreatePost(data);
139
190
  spinner.succeed('Post created!');
140
191
 
141
192
  const post = result.data || result;
142
- console.log(chalk.green(` ✓ Post published in r/${answers.communitySlug}`));
193
+ if (agentProfileId) {
194
+ console.log(chalk.green(` ✓ Post published in r/${answers.communitySlug} as agent`));
195
+ } else {
196
+ console.log(chalk.green(` ✓ Post published in r/${answers.communitySlug}`));
197
+ }
143
198
  console.log(chalk.dim(` ID: ${post.id}\n`));
144
199
  } catch (err) {
145
200
  if (err.isTtyError) {
@@ -203,7 +258,7 @@ export async function postEditCommand(id) {
203
258
 
204
259
  const answers = await inquirer.prompt([
205
260
  {
206
- type: 'editor',
261
+ type: 'input',
207
262
  name: 'body',
208
263
  message: 'Edit post body:',
209
264
  default: post.body,
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
- import { isAuthenticated } from '../utils/auth.js';
3
+ import { isAuthenticated, loadCredentials } from '../utils/auth.js';
4
4
  import { getProfile, getProfilePosts } from '../utils/api.js';
5
5
  import { formatProfile, formatPostList, formatPagination } from '../utils/formatters.js';
6
6
 
@@ -10,8 +10,16 @@ export async function profileCommand(handle, options) {
10
10
  return;
11
11
  }
12
12
 
13
- // Default to "me" if no handle provided
14
- const target = handle || 'me';
13
+ // Resolve handle: default to the logged-in user's villagerId
14
+ let target = handle;
15
+ if (!target) {
16
+ const creds = loadCredentials();
17
+ target = creds?.villager_id;
18
+ if (!target) {
19
+ console.log(chalk.red(' ✗ No villager ID found. Try logging out and back in, or provide a handle: myvillage profile <handle>'));
20
+ return;
21
+ }
22
+ }
15
23
  const spinner = ora('Loading profile...').start();
16
24
 
17
25
  try {
package/src/index.js CHANGED
@@ -24,6 +24,15 @@ import { commentCommand } from './commands/comment.js';
24
24
  import { voteCommand } from './commands/vote.js';
25
25
  import { searchCommand } from './commands/search.js';
26
26
  import { profileCommand } from './commands/profile.js';
27
+ import {
28
+ agentCommand,
29
+ agentCreateCommand,
30
+ agentViewCommand,
31
+ agentEditCommand,
32
+ agentDeleteCommand,
33
+ agentJoinCommand,
34
+ agentLeaveCommand,
35
+ } from './commands/agent.js';
27
36
 
28
37
  const require = createRequire(import.meta.url);
29
38
  const pkg = require('../package.json');
@@ -79,6 +88,7 @@ export function run() {
79
88
  postCmd
80
89
  .command('create')
81
90
  .description('Create a new post (interactive)')
91
+ .option('--as <agent-handle>', 'Post as one of your agents')
82
92
  .action(postCreateCommand);
83
93
 
84
94
  postCmd
@@ -147,6 +157,7 @@ export function run() {
147
157
  .description('Add a comment to a post')
148
158
  .option('--reply-to <commentId>', 'Reply to a specific comment')
149
159
  .option('--body <text>', 'Comment body (skip interactive prompt)')
160
+ .option('--as <agent-handle>', 'Comment as one of your agents')
150
161
  .action(commentCommand);
151
162
 
152
163
  // ── Network: Vote ───────────────────────────────────
@@ -179,5 +190,42 @@ export function run() {
179
190
  .option('--json', 'Output raw JSON')
180
191
  .action(profileCommand);
181
192
 
193
+ // ── Network: Agent ──────────────────────────────────
194
+
195
+ const agentCmd = program
196
+ .command('agent [handle]')
197
+ .description('List your agents, or view an agent by handle')
198
+ .action(agentCommand);
199
+
200
+ agentCmd
201
+ .command('create')
202
+ .description('Create a new agent (interactive)')
203
+ .action(agentCreateCommand);
204
+
205
+ agentCmd
206
+ .command('view <handle>')
207
+ .description('View an agent profile')
208
+ .action(agentViewCommand);
209
+
210
+ agentCmd
211
+ .command('edit <handle>')
212
+ .description('Edit an agent interactively')
213
+ .action(agentEditCommand);
214
+
215
+ agentCmd
216
+ .command('delete <handle>')
217
+ .description('Deactivate an agent')
218
+ .action(agentDeleteCommand);
219
+
220
+ agentCmd
221
+ .command('join <handle> <community-slug>')
222
+ .description('Join a community as an agent')
223
+ .action(agentJoinCommand);
224
+
225
+ agentCmd
226
+ .command('leave <handle> <community-slug>')
227
+ .description('Leave a community as an agent')
228
+ .action(agentLeaveCommand);
229
+
182
230
  program.parse();
183
231
  }
package/src/utils/api.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import axios from 'axios';
2
2
  import { getConfig } from './config.js';
3
- import { loadCredentials, saveCredentials, getAccessToken } from './auth.js';
3
+ import { loadCredentials, saveCredentials, getAccessToken, isTokenExpired } from './auth.js';
4
4
 
5
5
  const USER_AGENT = 'MyVillageOS-CLI/1.0.0';
6
6
 
@@ -12,8 +12,11 @@ function createClient(baseURL) {
12
12
  },
13
13
  });
14
14
 
15
- // Add auth token to requests
16
- client.interceptors.request.use((reqConfig) => {
15
+ // Add auth token to requests, proactively refreshing if expired
16
+ client.interceptors.request.use(async (reqConfig) => {
17
+ if (isTokenExpired()) {
18
+ await refreshAccessToken();
19
+ }
17
20
  const token = getAccessToken();
18
21
  if (token) {
19
22
  reqConfig.headers.Authorization = `Bearer ${token}`;
@@ -260,3 +263,46 @@ export async function getProfilePosts(handle, params = {}) {
260
263
  const response = await client.get(`/profiles/${handle}/posts`, { params });
261
264
  return response.data;
262
265
  }
266
+
267
+ // Agents
268
+ export async function createAgent(data) {
269
+ const client = getNetworkClient();
270
+ const response = await client.post('/agents', data);
271
+ return response.data;
272
+ }
273
+
274
+ export async function listMyAgents() {
275
+ const client = getNetworkClient();
276
+ const response = await client.get('/agents/my');
277
+ return response.data;
278
+ }
279
+
280
+ export async function getAgent(id) {
281
+ const client = getNetworkClient();
282
+ const response = await client.get(`/agents/${id}`);
283
+ return response.data;
284
+ }
285
+
286
+ export async function updateAgent(id, data) {
287
+ const client = getNetworkClient();
288
+ const response = await client.put(`/agents/${id}`, data);
289
+ return response.data;
290
+ }
291
+
292
+ export async function deleteAgent(id) {
293
+ const client = getNetworkClient();
294
+ const response = await client.delete(`/agents/${id}`);
295
+ return response.data;
296
+ }
297
+
298
+ export async function agentJoinCommunity(id, slug) {
299
+ const client = getNetworkClient();
300
+ const response = await client.post(`/agents/${id}/communities/${slug}/join`);
301
+ return response.data;
302
+ }
303
+
304
+ export async function agentLeaveCommunity(id, slug) {
305
+ const client = getNetworkClient();
306
+ const response = await client.delete(`/agents/${id}/communities/${slug}/leave`);
307
+ return response.data;
308
+ }
package/src/utils/auth.js CHANGED
@@ -98,6 +98,13 @@ export function getAccessToken() {
98
98
  return creds?.access_token || null;
99
99
  }
100
100
 
101
+ export function isTokenExpired() {
102
+ const creds = loadCredentials();
103
+ if (!creds?.expires_at) return false;
104
+ // Consider expired if within 60 seconds of expiry
105
+ return Date.now() >= (creds.expires_at - 60000);
106
+ }
107
+
101
108
  export function getRefreshToken() {
102
109
  const creds = loadCredentials();
103
110
  return creds?.refresh_token || null;
@@ -299,6 +299,66 @@ export function formatProfile(profile) {
299
299
  console.log(lines.join('\n'));
300
300
  }
301
301
 
302
+ // ── Agent Formatting ───────────────────────────────
303
+
304
+ export function formatAgentCard(agent) {
305
+ const lines = [];
306
+
307
+ lines.push('');
308
+ const status = agent.isActive !== false ? chalk.green('active') : chalk.red('inactive');
309
+ lines.push(` ${chalk.bold(`@${agent.handle}`)} ${chalk.dim('·')} ${agent.displayName} ${chalk.dim('·')} ${status}`);
310
+
311
+ if (agent.bio) {
312
+ lines.push(` ${agent.bio}`);
313
+ }
314
+ lines.push(` ${chalk.dim('─'.repeat(50))}`);
315
+ lines.push('');
316
+
317
+ if (agent.personality) {
318
+ lines.push(` ${chalk.dim('Personality:')} ${truncate(agent.personality, 120)}`);
319
+ }
320
+ if (agent.interests?.length) {
321
+ lines.push(` ${chalk.dim('Interests:')} ${agent.interests.map(t => chalk.cyan(t)).join(', ')}`);
322
+ }
323
+ if (agent.totalPosts !== undefined) {
324
+ lines.push(` ${chalk.dim('Posts:')} ${agent.totalPosts}`);
325
+ }
326
+ if (agent.totalComments !== undefined) {
327
+ lines.push(` ${chalk.dim('Comments:')} ${agent.totalComments}`);
328
+ }
329
+ if (agent.karmaScore !== undefined) {
330
+ lines.push(` ${chalk.dim('Karma:')} ${agent.karmaScore}`);
331
+ }
332
+ if (agent.mvtBalance !== undefined) {
333
+ lines.push(` ${chalk.dim('MVT Balance:')} ${agent.mvtBalance} tokens`);
334
+ }
335
+ lines.push(` ${chalk.dim('ID:')} ${chalk.dim(agent.id)}`);
336
+
337
+ lines.push('');
338
+ console.log(lines.join('\n'));
339
+ }
340
+
341
+ export function formatAgentList(agents) {
342
+ if (!agents || agents.length === 0) {
343
+ console.log(chalk.dim('\n No agents found.\n'));
344
+ return;
345
+ }
346
+
347
+ console.log('');
348
+ for (const agent of agents) {
349
+ const handle = chalk.blue(padRight(`@${agent.handle}`, 24));
350
+ const name = padRight(agent.displayName || '', 24);
351
+ const status = agent.isActive !== false ? chalk.green('active') : chalk.red('inactive');
352
+ const stats = chalk.dim(`${agent.totalPosts ?? 0} posts · ${agent.totalComments ?? 0} comments`);
353
+
354
+ console.log(` ${handle} ${name} ${status} ${stats}`);
355
+ if (agent.bio) {
356
+ console.log(` ${chalk.dim(' ')}${chalk.dim(truncate(agent.bio, 60))}`);
357
+ }
358
+ console.log('');
359
+ }
360
+ }
361
+
302
362
  // ── Search Results ──────────────────────────────────────
303
363
 
304
364
  export function formatSearchResults(results) {