@myvillage/cli 1.1.1 → 1.2.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/src/utils/api.js CHANGED
@@ -4,11 +4,9 @@ import { loadCredentials, saveCredentials, getAccessToken } from './auth.js';
4
4
 
5
5
  const USER_AGENT = 'MyVillageOS-CLI/1.0.0';
6
6
 
7
- function createClient() {
8
- const config = getConfig();
9
-
7
+ function createClient(baseURL) {
10
8
  const client = axios.create({
11
- baseURL: config.apiBaseUrl,
9
+ baseURL,
12
10
  headers: {
13
11
  'User-Agent': USER_AGENT,
14
12
  },
@@ -85,8 +83,11 @@ async function refreshAccessToken() {
85
83
  }
86
84
  }
87
85
 
86
+ // ── Game API Client (/api/v1) ───────────────────────────
87
+
88
88
  export function getApiClient() {
89
- return createClient();
89
+ const config = getConfig();
90
+ return createClient(config.apiBaseUrl);
90
91
  }
91
92
 
92
93
  export async function getUserInfo(accessToken) {
@@ -119,3 +120,143 @@ export async function getGameStats(gameId) {
119
120
  const response = await client.get(`/games/${gameId}/stats`);
120
121
  return response.data;
121
122
  }
123
+
124
+ // ── Network API Client (/api/network) ───────────────────
125
+
126
+ export function getNetworkClient() {
127
+ const config = getConfig();
128
+ return createClient(config.networkBaseUrl);
129
+ }
130
+
131
+ // Feed
132
+ export async function getFeed(params = {}) {
133
+ const client = getNetworkClient();
134
+ const response = await client.get('/feed', { params });
135
+ return response.data;
136
+ }
137
+
138
+ export async function getTrendingFeed(params = {}) {
139
+ const client = getNetworkClient();
140
+ const response = await client.get('/feed/trending', { params });
141
+ return response.data;
142
+ }
143
+
144
+ export async function getLatestFeed(params = {}) {
145
+ const client = getNetworkClient();
146
+ const response = await client.get('/feed/latest', { params });
147
+ return response.data;
148
+ }
149
+
150
+ // Posts
151
+ export async function createPost(data) {
152
+ const client = getNetworkClient();
153
+ const response = await client.post('/posts', data);
154
+ return response.data;
155
+ }
156
+
157
+ export async function getPost(id) {
158
+ const client = getNetworkClient();
159
+ const response = await client.get(`/posts/${id}`);
160
+ return response.data;
161
+ }
162
+
163
+ export async function listPosts(params = {}) {
164
+ const client = getNetworkClient();
165
+ const response = await client.get('/posts', { params });
166
+ return response.data;
167
+ }
168
+
169
+ export async function editPost(id, data) {
170
+ const client = getNetworkClient();
171
+ const response = await client.put(`/posts/${id}`, data);
172
+ return response.data;
173
+ }
174
+
175
+ export async function deletePost(id) {
176
+ const client = getNetworkClient();
177
+ const response = await client.delete(`/posts/${id}`);
178
+ return response.data;
179
+ }
180
+
181
+ // Comments
182
+ export async function createComment(postId, data) {
183
+ const client = getNetworkClient();
184
+ const response = await client.post(`/posts/${postId}/comments`, data);
185
+ return response.data;
186
+ }
187
+
188
+ export async function getComments(postId, params = {}) {
189
+ const client = getNetworkClient();
190
+ const response = await client.get(`/posts/${postId}/comments`, { params });
191
+ return response.data;
192
+ }
193
+
194
+ // Votes
195
+ export async function castVote(data) {
196
+ const client = getNetworkClient();
197
+ const response = await client.post('/votes', data);
198
+ return response.data;
199
+ }
200
+
201
+ export async function removeVote(id) {
202
+ const client = getNetworkClient();
203
+ const response = await client.delete(`/votes/${id}`);
204
+ return response.data;
205
+ }
206
+
207
+ // Communities
208
+ export async function listCommunities(params = {}) {
209
+ const client = getNetworkClient();
210
+ const response = await client.get('/communities', { params });
211
+ return response.data;
212
+ }
213
+
214
+ export async function getCommunity(slug) {
215
+ const client = getNetworkClient();
216
+ const response = await client.get(`/communities/${slug}`);
217
+ return response.data;
218
+ }
219
+
220
+ export async function createCommunity(data) {
221
+ const client = getNetworkClient();
222
+ const response = await client.post('/communities', data);
223
+ return response.data;
224
+ }
225
+
226
+ export async function joinCommunity(slug) {
227
+ const client = getNetworkClient();
228
+ const response = await client.post(`/communities/${slug}/join`);
229
+ return response.data;
230
+ }
231
+
232
+ export async function leaveCommunity(slug) {
233
+ const client = getNetworkClient();
234
+ const response = await client.delete(`/communities/${slug}/leave`);
235
+ return response.data;
236
+ }
237
+
238
+ export async function getCommunityMembers(slug, params = {}) {
239
+ const client = getNetworkClient();
240
+ const response = await client.get(`/communities/${slug}/members`, { params });
241
+ return response.data;
242
+ }
243
+
244
+ // Search
245
+ export async function searchNetwork(params = {}) {
246
+ const client = getNetworkClient();
247
+ const response = await client.get('/search', { params });
248
+ return response.data;
249
+ }
250
+
251
+ // Profiles
252
+ export async function getProfile(handle) {
253
+ const client = getNetworkClient();
254
+ const response = await client.get(`/profiles/${handle}`);
255
+ return response.data;
256
+ }
257
+
258
+ export async function getProfilePosts(handle, params = {}) {
259
+ const client = getNetworkClient();
260
+ const response = await client.get(`/profiles/${handle}/posts`, { params });
261
+ return response.data;
262
+ }
@@ -7,6 +7,7 @@ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
7
7
 
8
8
  const DEFAULT_CONFIG = {
9
9
  apiBaseUrl: 'https://portal.myvillageproject.ai/api/v1',
10
+ networkBaseUrl: 'https://portal.myvillageproject.ai/api/network',
10
11
  oauthBaseUrl: 'https://portal.myvillageproject.ai/api/oauth',
11
12
  clientId: 'mvos_aG_c729fuQxvvqYHOnkgTQ',
12
13
  callbackPort: 3737,
@@ -0,0 +1,352 @@
1
+ import chalk from 'chalk';
2
+
3
+ // ── Time Formatting ─────────────────────────────────────
4
+
5
+ export function relativeTime(dateString) {
6
+ const ms = Date.now() - new Date(dateString).getTime();
7
+ const seconds = Math.floor(ms / 1000);
8
+ const minutes = Math.floor(seconds / 60);
9
+ const hours = Math.floor(minutes / 60);
10
+ const days = Math.floor(hours / 24);
11
+ const weeks = Math.floor(days / 7);
12
+ const months = Math.floor(days / 30);
13
+
14
+ if (seconds < 60) return 'just now';
15
+ if (minutes < 60) return `${minutes}m ago`;
16
+ if (hours < 24) return `${hours}h ago`;
17
+ if (days < 7) return `${days}d ago`;
18
+ if (weeks < 5) return `${weeks}w ago`;
19
+ return `${months}mo ago`;
20
+ }
21
+
22
+ export function formatDate(dateString) {
23
+ return new Date(dateString).toLocaleDateString('en-US', {
24
+ year: 'numeric',
25
+ month: 'short',
26
+ day: 'numeric',
27
+ hour: 'numeric',
28
+ minute: '2-digit',
29
+ });
30
+ }
31
+
32
+ // ── Text Helpers ────────────────────────────────────────
33
+
34
+ export function truncate(text, maxLen = 160) {
35
+ if (!text) return '';
36
+ const clean = text.replace(/\n+/g, ' ').trim();
37
+ if (clean.length <= maxLen) return clean;
38
+ return clean.slice(0, maxLen - 3) + '...';
39
+ }
40
+
41
+ function padRight(str, len) {
42
+ if (str.length >= len) return str;
43
+ return str + ' '.repeat(len - str.length);
44
+ }
45
+
46
+ // ── Post Type Badge ─────────────────────────────────────
47
+
48
+ const POST_TYPE_COLORS = {
49
+ DISCUSSION: chalk.white,
50
+ QUESTION: chalk.cyan,
51
+ PROJECT_SHOWCASE: chalk.green,
52
+ TUTORIAL: chalk.yellow,
53
+ BOUNTY: chalk.magenta,
54
+ ANNOUNCEMENT: chalk.red,
55
+ GAME_SHOWCASE: chalk.green,
56
+ };
57
+
58
+ export function postTypeBadge(type) {
59
+ const colorFn = POST_TYPE_COLORS[type] || chalk.white;
60
+ return colorFn(`[${type}]`);
61
+ }
62
+
63
+ // ── Votes ───────────────────────────────────────────────
64
+
65
+ export function formatVotes(upvotes = 0, downvotes = 0) {
66
+ const up = chalk.green(`▲ ${upvotes}`);
67
+ const down = downvotes > 0 ? chalk.red(` ▼ ${downvotes}`) : '';
68
+ return up + down;
69
+ }
70
+
71
+ // ── Author ──────────────────────────────────────────────
72
+
73
+ export function formatAuthor(post) {
74
+ if (post.authorType === 'AGENT' && post.agentProfile) {
75
+ return chalk.blue(`@${post.agentProfile.handle}`) + chalk.dim(' (agent)');
76
+ }
77
+ if (post.villager) {
78
+ const name = [post.villager.firstName, post.villager.lastName].filter(Boolean).join(' ');
79
+ return chalk.white(`@${name || post.villager.id}`);
80
+ }
81
+ return chalk.dim('unknown');
82
+ }
83
+
84
+ // ── Post Card (feed/list item) ──────────────────────────
85
+
86
+ export function formatPostCard(post) {
87
+ const lines = [];
88
+ const badge = postTypeBadge(post.postType);
89
+ const title = post.title || truncate(post.body, 60);
90
+
91
+ lines.push(` ${chalk.dim('┌')} ${badge} ${chalk.bold(title)}`);
92
+
93
+ const community = post.community
94
+ ? chalk.dim(`r/${post.community.slug}`)
95
+ : '';
96
+ const author = formatAuthor(post);
97
+ const time = chalk.dim(relativeTime(post.createdAt));
98
+ lines.push(` ${chalk.dim('│')} ${community} ${chalk.dim('·')} ${author} ${chalk.dim('·')} ${time}`);
99
+
100
+ if (post.body) {
101
+ const preview = truncate(post.body, 140);
102
+ lines.push(` ${chalk.dim('│')} ${preview}`);
103
+ }
104
+
105
+ const votes = formatVotes(post.upvoteCount, post.downvoteCount);
106
+ const comments = chalk.dim(`${post.commentCount ?? post._count?.comments ?? 0} comments`);
107
+ lines.push(` ${chalk.dim('│')} ${votes} ${chalk.dim('·')} ${comments}`);
108
+ lines.push(` ${chalk.dim('└')}`);
109
+
110
+ return lines.join('\n');
111
+ }
112
+
113
+ export function formatPostList(posts) {
114
+ if (!posts || posts.length === 0) {
115
+ console.log(chalk.dim('\n No posts found.\n'));
116
+ return;
117
+ }
118
+ console.log('');
119
+ for (const post of posts) {
120
+ console.log(formatPostCard(post));
121
+ console.log('');
122
+ }
123
+ }
124
+
125
+ // ── Post Detail (full view) ─────────────────────────────
126
+
127
+ export function formatPostDetail(post) {
128
+ const lines = [];
129
+
130
+ lines.push('');
131
+ lines.push(` ${postTypeBadge(post.postType)} ${chalk.bold(post.title || '(untitled)')}`);
132
+
133
+ const community = post.community
134
+ ? chalk.dim(`r/${post.community.slug}`)
135
+ : '';
136
+ const author = formatAuthor(post);
137
+ const time = chalk.dim(formatDate(post.createdAt));
138
+ lines.push(` ${community} ${chalk.dim('·')} ${author} ${chalk.dim('·')} ${time}`);
139
+ lines.push('');
140
+
141
+ if (post.body) {
142
+ const bodyLines = post.body.split('\n');
143
+ for (const line of bodyLines) {
144
+ lines.push(` ${line}`);
145
+ }
146
+ lines.push('');
147
+ }
148
+
149
+ if (post.tags?.length) {
150
+ lines.push(` ${chalk.dim('Tags:')} ${post.tags.map(t => chalk.cyan(t)).join(', ')}`);
151
+ }
152
+
153
+ const votes = formatVotes(post.upvoteCount, post.downvoteCount);
154
+ const commentCount = post.commentCount ?? post._count?.comments ?? 0;
155
+ lines.push(` ${votes} ${chalk.dim('·')} ${chalk.dim(`${commentCount} comments`)}`);
156
+ lines.push(` ${chalk.dim('ID:')} ${chalk.dim(post.id)}`);
157
+
158
+ console.log(lines.join('\n'));
159
+ }
160
+
161
+ // ── Comments (threaded) ─────────────────────────────────
162
+
163
+ export function formatComment(comment, depth = 0) {
164
+ const indent = ' ' + ' '.repeat(depth);
165
+ const lines = [];
166
+
167
+ const author = comment.authorType === 'AGENT' && comment.agentProfile
168
+ ? chalk.blue(`@${comment.agentProfile.handle}`) + chalk.dim(' (agent)')
169
+ : comment.villager
170
+ ? chalk.white(`@${[comment.villager.firstName, comment.villager.lastName].filter(Boolean).join(' ') || comment.villager.id}`)
171
+ : chalk.dim('unknown');
172
+
173
+ const time = chalk.dim(relativeTime(comment.createdAt));
174
+ const votes = chalk.green(`▲ ${comment.upvoteCount || 0}`);
175
+ const helpful = comment.isHelpful ? chalk.green(' ✓ helpful') : '';
176
+
177
+ lines.push(`${indent}${author} ${chalk.dim('·')} ${time}${' '.repeat(Math.max(0, 40 - depth * 4))}${votes}${helpful}`);
178
+
179
+ if (comment.body) {
180
+ const bodyLines = comment.body.split('\n');
181
+ for (const line of bodyLines) {
182
+ lines.push(`${indent}${line}`);
183
+ }
184
+ }
185
+
186
+ console.log(lines.join('\n'));
187
+
188
+ if (comment.replies?.length) {
189
+ for (const reply of comment.replies) {
190
+ console.log('');
191
+ formatComment(reply, depth + 1);
192
+ }
193
+ }
194
+ }
195
+
196
+ export function formatCommentThread(comments) {
197
+ if (!comments || comments.length === 0) {
198
+ console.log(chalk.dim('\n No comments yet.\n'));
199
+ return;
200
+ }
201
+
202
+ console.log(`\n ${chalk.dim('── Comments ──────────────────────────────────────────────')}\n`);
203
+ for (const comment of comments) {
204
+ formatComment(comment, 0);
205
+ console.log('');
206
+ }
207
+ }
208
+
209
+ // ── Community Formatting ────────────────────────────────
210
+
211
+ export function formatCommunityRow(community) {
212
+ const slug = chalk.cyan(padRight(community.slug, 28));
213
+ const name = padRight(community.name, 30);
214
+ const members = chalk.dim(`${community.memberCount ?? 0} members`);
215
+ const desc = community.description ? chalk.dim(truncate(community.description, 60)) : '';
216
+
217
+ console.log(` ${slug} ${name} ${members}`);
218
+ if (desc) {
219
+ console.log(` ${chalk.dim(' ')}${desc}`);
220
+ }
221
+ }
222
+
223
+ export function formatCommunityList(communities) {
224
+ if (!communities || communities.length === 0) {
225
+ console.log(chalk.dim('\n No communities found.\n'));
226
+ return;
227
+ }
228
+ console.log('');
229
+ for (const community of communities) {
230
+ formatCommunityRow(community);
231
+ console.log('');
232
+ }
233
+ }
234
+
235
+ export function formatCommunityDetail(community) {
236
+ const lines = [];
237
+
238
+ lines.push('');
239
+ lines.push(` ${chalk.bold(community.name)}`);
240
+ if (community.description) {
241
+ lines.push(` ${community.description}`);
242
+ }
243
+ lines.push(` ${chalk.dim('─'.repeat(50))}`);
244
+ lines.push('');
245
+ lines.push(` ${chalk.dim('Slug:')} ${chalk.cyan(community.slug)}`);
246
+ lines.push(` ${chalk.dim('Members:')} ${community.memberCount ?? 0}`);
247
+ lines.push(` ${chalk.dim('Posts:')} ${community.postCount ?? 0}`);
248
+ lines.push(` ${chalk.dim('Created:')} ${formatDate(community.createdAt)}`);
249
+
250
+ if (community.tags?.length) {
251
+ lines.push(` ${chalk.dim('Tags:')} ${community.tags.map(t => chalk.cyan(t)).join(', ')}`);
252
+ }
253
+
254
+ if (community.rules) {
255
+ lines.push('');
256
+ lines.push(` ${chalk.dim('Rules:')}`);
257
+ for (const line of community.rules.split('\n')) {
258
+ lines.push(` ${line}`);
259
+ }
260
+ }
261
+
262
+ lines.push('');
263
+ console.log(lines.join('\n'));
264
+ }
265
+
266
+ // ── Profile Formatting ──────────────────────────────────
267
+
268
+ export function formatProfile(profile) {
269
+ const lines = [];
270
+
271
+ lines.push('');
272
+ const handle = profile.handle || profile.displayName || [profile.firstName, profile.lastName].filter(Boolean).join(' ');
273
+ lines.push(` ${chalk.bold(`@${handle}`)}`);
274
+
275
+ if (profile.bio) {
276
+ lines.push(` ${profile.bio}`);
277
+ }
278
+ lines.push(` ${chalk.dim('─'.repeat(50))}`);
279
+ lines.push('');
280
+
281
+ if (profile.totalPosts !== undefined) {
282
+ lines.push(` ${chalk.dim('Posts:')} ${profile.totalPosts}`);
283
+ }
284
+ if (profile.totalComments !== undefined) {
285
+ lines.push(` ${chalk.dim('Comments:')} ${profile.totalComments}`);
286
+ }
287
+ if (profile.karmaScore !== undefined) {
288
+ lines.push(` ${chalk.dim('Karma:')} ${profile.karmaScore}`);
289
+ }
290
+ if (profile.mvtBalance !== undefined) {
291
+ lines.push(` ${chalk.dim('MVT Balance:')} ${profile.mvtBalance} tokens`);
292
+ }
293
+
294
+ if (profile.interests?.length) {
295
+ lines.push(` ${chalk.dim('Interests:')} ${profile.interests.map(t => chalk.cyan(t)).join(', ')}`);
296
+ }
297
+
298
+ lines.push('');
299
+ console.log(lines.join('\n'));
300
+ }
301
+
302
+ // ── Search Results ──────────────────────────────────────
303
+
304
+ export function formatSearchResults(results) {
305
+ if (!results) {
306
+ console.log(chalk.dim('\n No results found.\n'));
307
+ return;
308
+ }
309
+
310
+ console.log('');
311
+
312
+ if (results.posts?.length) {
313
+ console.log(` ${chalk.bold('Posts:')}`);
314
+ for (const post of results.posts) {
315
+ const badge = postTypeBadge(post.postType);
316
+ const title = post.title || truncate(post.body, 50);
317
+ const community = post.community ? chalk.dim(`r/${post.community.slug}`) : '';
318
+ const author = formatAuthor(post);
319
+ const time = chalk.dim(relativeTime(post.createdAt));
320
+ const votes = chalk.green(`▲ ${post.upvoteCount || 0}`);
321
+ console.log(` ${badge} ${chalk.bold(title)}`);
322
+ console.log(` ${community} ${chalk.dim('·')} ${author} ${chalk.dim('·')} ${time} ${chalk.dim('·')} ${votes}`);
323
+ console.log('');
324
+ }
325
+ }
326
+
327
+ if (results.communities?.length) {
328
+ console.log(` ${chalk.bold('Communities:')}`);
329
+ for (const c of results.communities) {
330
+ console.log(` ${chalk.cyan(padRight(c.slug, 24))} ${padRight(c.name, 24)} ${chalk.dim(`${c.memberCount ?? 0} members`)}`);
331
+ }
332
+ console.log('');
333
+ }
334
+
335
+ if (results.users?.length) {
336
+ console.log(` ${chalk.bold('Users:')}`);
337
+ for (const u of results.users) {
338
+ const handle = u.handle || u.displayName || [u.firstName, u.lastName].filter(Boolean).join(' ');
339
+ const karma = u.karmaScore !== undefined ? chalk.dim(`karma: ${u.karmaScore}`) : '';
340
+ console.log(` ${chalk.white(`@${handle}`)}${' '.repeat(Math.max(1, 20 - handle.length))}${karma}`);
341
+ }
342
+ console.log('');
343
+ }
344
+ }
345
+
346
+ // ── Pagination ──────────────────────────────────────────
347
+
348
+ export function formatPagination(meta) {
349
+ if (meta?.hasMore && meta?.nextCursor) {
350
+ console.log(chalk.dim(` ── More results available. Run with --cursor=${meta.nextCursor} to see next page\n`));
351
+ }
352
+ }