@myvillage/cli 1.5.1 → 1.6.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 +1 -1
- package/src/commands/bizreqs.js +1 -25
- package/src/commands/checkin.js +149 -0
- package/src/commands/discover.js +220 -0
- package/src/commands/log.js +59 -0
- package/src/commands/login.js +1 -0
- package/src/commands/story.js +68 -0
- package/src/index.js +28 -0
- package/src/utils/ai.js +30 -0
- package/src/utils/api.js +31 -0
- package/src/utils/config.js +1 -0
- package/src/utils/formatters.js +85 -0
- package/src/utils/man-hooks.js +16 -0
- package/src/utils/village-resolver.js +104 -0
package/package.json
CHANGED
package/src/commands/bizreqs.js
CHANGED
|
@@ -100,31 +100,7 @@ const recommendationParams = z.object({
|
|
|
100
100
|
|
|
101
101
|
// ── Helper: Get Anthropic client ────────────────────────
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
const { createAnthropic } = await import('@ai-sdk/anthropic');
|
|
105
|
-
const config = getConfig();
|
|
106
|
-
let apiKey = config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
|
|
107
|
-
|
|
108
|
-
if (!apiKey) {
|
|
109
|
-
console.log(chalk.yellow('\n Anthropic API key required for AI-powered intake.'));
|
|
110
|
-
console.log(brand.teal(' Get yours at: https://console.anthropic.com\n'));
|
|
111
|
-
|
|
112
|
-
const { key } = await inquirer.prompt([{
|
|
113
|
-
type: 'password',
|
|
114
|
-
name: 'key',
|
|
115
|
-
message: 'Anthropic API key:',
|
|
116
|
-
mask: '*',
|
|
117
|
-
validate: (input) => input.trim().length > 0 || 'API key is required',
|
|
118
|
-
}]);
|
|
119
|
-
|
|
120
|
-
apiKey = key.trim();
|
|
121
|
-
const { setConfig } = await import('../utils/config.js');
|
|
122
|
-
setConfig({ anthropicApiKey: apiKey });
|
|
123
|
-
console.log(brand.green(' ✓ API key saved to ~/.myvillage/config.json\n'));
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return createAnthropic({ apiKey });
|
|
127
|
-
}
|
|
103
|
+
import { getAnthropicProvider } from '../utils/ai.js';
|
|
128
104
|
|
|
129
105
|
|
|
130
106
|
// ── bizreqs list ────────────────────────────────────────
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { isAuthenticated } from '../utils/auth.js';
|
|
5
|
+
import { brand, villageSpinner } from '../utils/brand.js';
|
|
6
|
+
import { createPost } from '../utils/api.js';
|
|
7
|
+
import { resolveVillageContext } from '../utils/village-resolver.js';
|
|
8
|
+
import { getAnthropicProvider } from '../utils/ai.js';
|
|
9
|
+
import { formatCheckinSummary } from '../utils/formatters.js';
|
|
10
|
+
import { discoverLogic } from './discover.js';
|
|
11
|
+
|
|
12
|
+
const extractionSchema = z.object({
|
|
13
|
+
themes: z.array(z.string()).describe('Key themes from the check-in (1-4 short labels)'),
|
|
14
|
+
needs: z.array(z.string()).describe('Things the village needs help with (0-3 short labels)'),
|
|
15
|
+
offers: z.array(z.string()).describe('Things the village can offer others (0-3 short labels)'),
|
|
16
|
+
buildTrack: z.string().optional().describe('Primary build track if mentioned: game, portal, agent, data, robot, or general'),
|
|
17
|
+
summary: z.string().describe('A 2-3 sentence summary combining all responses into a cohesive check-in update'),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export async function checkinCommand(options) {
|
|
21
|
+
if (!isAuthenticated()) {
|
|
22
|
+
console.log(chalk.red(" \u2717 Authentication required. Run 'myvillage login' first."));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ctx = await resolveVillageContext();
|
|
27
|
+
if (!ctx) return;
|
|
28
|
+
|
|
29
|
+
console.log();
|
|
30
|
+
console.log(brand.gold(' \u2743 Village Check-in'));
|
|
31
|
+
console.log(chalk.dim(' Answer a few questions about your village. AI will help organize it.\n'));
|
|
32
|
+
|
|
33
|
+
// Conversational prompts
|
|
34
|
+
const answers = await inquirer.prompt([
|
|
35
|
+
{
|
|
36
|
+
type: 'input',
|
|
37
|
+
name: 'work',
|
|
38
|
+
message: 'What did your village work on recently?',
|
|
39
|
+
validate: (v) => v.trim().length > 0 || 'Please share what you worked on.',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
type: 'input',
|
|
43
|
+
name: 'stuck',
|
|
44
|
+
message: 'Any problems you\'re stuck on?',
|
|
45
|
+
default: '',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: 'input',
|
|
49
|
+
name: 'share',
|
|
50
|
+
message: 'Anything you want to share with the network?',
|
|
51
|
+
default: '',
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
// AI structuring
|
|
56
|
+
const spinner = villageSpinner('Organizing your check-in...').start();
|
|
57
|
+
|
|
58
|
+
let extracted;
|
|
59
|
+
try {
|
|
60
|
+
const anthropic = await getAnthropicProvider();
|
|
61
|
+
const { generateText } = await import('ai');
|
|
62
|
+
|
|
63
|
+
const userContent = [
|
|
64
|
+
`What we worked on: ${answers.work}`,
|
|
65
|
+
answers.stuck ? `Problems we're stuck on: ${answers.stuck}` : null,
|
|
66
|
+
answers.share ? `What we want to share: ${answers.share}` : null,
|
|
67
|
+
].filter(Boolean).join('\n');
|
|
68
|
+
|
|
69
|
+
const result = await generateText({
|
|
70
|
+
model: anthropic('claude-haiku-4-5-20251001'),
|
|
71
|
+
system: `You are a community network coordinator. Extract structured metadata from a village check-in and write a concise summary. Always call the extract_checkin tool with your analysis.`,
|
|
72
|
+
messages: [{ role: 'user', content: userContent }],
|
|
73
|
+
tools: {
|
|
74
|
+
extract_checkin: {
|
|
75
|
+
description: 'Extract structured metadata from a village check-in',
|
|
76
|
+
parameters: extractionSchema,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
toolChoice: { type: 'tool', toolName: 'extract_checkin' },
|
|
80
|
+
maxTokens: 500,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const toolCall = result.toolCalls?.[0];
|
|
84
|
+
if (!toolCall?.args) {
|
|
85
|
+
throw new Error('AI did not return structured data');
|
|
86
|
+
}
|
|
87
|
+
extracted = toolCall.args;
|
|
88
|
+
spinner.succeed('Check-in organized');
|
|
89
|
+
} catch (err) {
|
|
90
|
+
spinner.fail('AI structuring failed — posting raw check-in');
|
|
91
|
+
// Fallback: post without AI structuring
|
|
92
|
+
extracted = {
|
|
93
|
+
themes: [],
|
|
94
|
+
needs: [],
|
|
95
|
+
offers: [],
|
|
96
|
+
buildTrack: 'general',
|
|
97
|
+
summary: [answers.work, answers.stuck, answers.share].filter(Boolean).join(' '),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Build tags
|
|
102
|
+
const tags = ['checkin'];
|
|
103
|
+
if (extracted.buildTrack) tags.push(`track:${extracted.buildTrack}`);
|
|
104
|
+
for (const t of extracted.themes) tags.push(`theme:${t}`);
|
|
105
|
+
for (const n of extracted.needs) tags.push(`need:${n}`);
|
|
106
|
+
for (const o of extracted.offers) tags.push(`offer:${o}`);
|
|
107
|
+
|
|
108
|
+
// Create post
|
|
109
|
+
try {
|
|
110
|
+
const post = await createPost({
|
|
111
|
+
communitySlug: ctx.community.slug,
|
|
112
|
+
postType: 'DISCUSSION',
|
|
113
|
+
title: `Village Check-in: ${ctx.village.name}`,
|
|
114
|
+
body: extracted.summary,
|
|
115
|
+
tags,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
formatCheckinSummary(post, {
|
|
119
|
+
villageName: ctx.village.name,
|
|
120
|
+
buildTrack: extracted.buildTrack,
|
|
121
|
+
themes: extracted.themes,
|
|
122
|
+
needs: extracted.needs,
|
|
123
|
+
offers: extracted.offers,
|
|
124
|
+
});
|
|
125
|
+
} catch (err) {
|
|
126
|
+
const msg = err.response?.data?.error || err.message;
|
|
127
|
+
console.log(chalk.red(` \u2717 Failed to post check-in: ${msg}`));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Auto-discover prompt
|
|
132
|
+
if (!options.skipDiscover) {
|
|
133
|
+
const { runDiscover } = await inquirer.prompt([{
|
|
134
|
+
type: 'confirm',
|
|
135
|
+
name: 'runDiscover',
|
|
136
|
+
message: 'Look for connections with other villages?',
|
|
137
|
+
default: true,
|
|
138
|
+
}]);
|
|
139
|
+
|
|
140
|
+
if (runDiscover) {
|
|
141
|
+
await discoverLogic(ctx, {
|
|
142
|
+
themes: extracted.themes,
|
|
143
|
+
needs: extracted.needs,
|
|
144
|
+
offers: extracted.offers,
|
|
145
|
+
track: extracted.buildTrack,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { isAuthenticated, loadCredentials } from '../utils/auth.js';
|
|
4
|
+
import { brand, villageSpinner } from '../utils/brand.js';
|
|
5
|
+
import {
|
|
6
|
+
listPosts,
|
|
7
|
+
listPostsByFilters,
|
|
8
|
+
createPost,
|
|
9
|
+
getVillagerVillages,
|
|
10
|
+
getVillageCommunities,
|
|
11
|
+
} from '../utils/api.js';
|
|
12
|
+
import { resolveVillageContext } from '../utils/village-resolver.js';
|
|
13
|
+
import { formatConnection } from '../utils/formatters.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse prefixed tags into a structured map.
|
|
17
|
+
* e.g., ['checkin', 'track:game', 'theme:ai', 'need:mentors'] →
|
|
18
|
+
* { themes: ['ai'], needs: ['mentors'], offers: [], track: 'game' }
|
|
19
|
+
*/
|
|
20
|
+
function parseTags(tags = []) {
|
|
21
|
+
const result = { themes: [], needs: [], offers: [], track: null };
|
|
22
|
+
for (const tag of tags) {
|
|
23
|
+
if (tag.startsWith('theme:')) result.themes.push(tag.replace('theme:', ''));
|
|
24
|
+
else if (tag.startsWith('need:')) result.needs.push(tag.replace('need:', ''));
|
|
25
|
+
else if (tag.startsWith('offer:')) result.offers.push(tag.replace('offer:', ''));
|
|
26
|
+
else if (tag.startsWith('track:')) result.track = tag.replace('track:', '');
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Score a candidate post against the user's tags.
|
|
33
|
+
*/
|
|
34
|
+
function scoreMatch(myTags, theirTags) {
|
|
35
|
+
let score = 0;
|
|
36
|
+
const matchTypes = [];
|
|
37
|
+
|
|
38
|
+
// Need-to-offer matches (3 points each)
|
|
39
|
+
for (const need of myTags.needs) {
|
|
40
|
+
if (theirTags.offers.includes(need)) {
|
|
41
|
+
score += 3;
|
|
42
|
+
matchTypes.push(`They offer "${need}" (you need it)`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (const offer of myTags.offers) {
|
|
46
|
+
if (theirTags.needs.includes(offer)) {
|
|
47
|
+
score += 3;
|
|
48
|
+
matchTypes.push(`They need "${offer}" (you offer it)`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Same build track (2 points)
|
|
53
|
+
if (myTags.track && theirTags.track && myTags.track === theirTags.track) {
|
|
54
|
+
score += 2;
|
|
55
|
+
matchTypes.push(`Same track: ${myTags.track}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Shared themes (1 point each)
|
|
59
|
+
for (const theme of myTags.themes) {
|
|
60
|
+
if (theirTags.themes.includes(theme)) {
|
|
61
|
+
score += 1;
|
|
62
|
+
matchTypes.push(`Shared theme: ${theme}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { score, matchTypes };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Core discover logic, shared between standalone and post-checkin invocation.
|
|
71
|
+
*/
|
|
72
|
+
export async function discoverLogic(ctx, myTags) {
|
|
73
|
+
const spinner = villageSpinner('Searching for connections...').start();
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const creds = loadCredentials();
|
|
77
|
+
|
|
78
|
+
// 1. Get all the user's villages' communities (for cross-village search)
|
|
79
|
+
const villagesResult = await getVillagerVillages(ctx.villagerUuid);
|
|
80
|
+
const allVillages = villagesResult?.data || [];
|
|
81
|
+
|
|
82
|
+
const communityIds = [];
|
|
83
|
+
for (const village of allVillages) {
|
|
84
|
+
const commResult = await getVillageCommunities(village.id);
|
|
85
|
+
const comms = commResult?.data || [];
|
|
86
|
+
for (const c of comms) {
|
|
87
|
+
if (!communityIds.includes(c.id)) {
|
|
88
|
+
communityIds.push(c.id);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (communityIds.length === 0) {
|
|
94
|
+
spinner.fail('No village communities found to search.');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 2. Fetch recent posts (last 14 days)
|
|
99
|
+
const since = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString();
|
|
100
|
+
const postsResult = await listPostsByFilters({
|
|
101
|
+
communityIds: communityIds.join(','),
|
|
102
|
+
tags: 'checkin,build-log,knowledge-contribution',
|
|
103
|
+
since,
|
|
104
|
+
pageSize: 50,
|
|
105
|
+
sort: 'new',
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const posts = postsResult?.data || [];
|
|
109
|
+
|
|
110
|
+
// 3. Filter out current user's own posts
|
|
111
|
+
const otherPosts = posts.filter((p) => p.villagerId !== ctx.villagerUuid);
|
|
112
|
+
|
|
113
|
+
if (otherPosts.length === 0) {
|
|
114
|
+
spinner.info('No connections found right now \u2014 check back after more villages check in.');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 4. Score and rank matches
|
|
119
|
+
const scored = otherPosts.map((post) => {
|
|
120
|
+
const theirTags = parseTags(post.tags);
|
|
121
|
+
const { score, matchTypes } = scoreMatch(myTags, theirTags);
|
|
122
|
+
const villageName = post.community?.name || 'Unknown Village';
|
|
123
|
+
return { post, score, matchTypes, villageName, theirTags };
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const topMatches = scored
|
|
127
|
+
.filter((m) => m.score > 0)
|
|
128
|
+
.sort((a, b) => b.score - a.score)
|
|
129
|
+
.slice(0, 3);
|
|
130
|
+
|
|
131
|
+
spinner.stop();
|
|
132
|
+
|
|
133
|
+
if (topMatches.length === 0) {
|
|
134
|
+
console.log(chalk.dim('\n No connections found right now \u2014 check back after more villages check in.\n'));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(brand.gold(`\n Found ${topMatches.length} connection${topMatches.length > 1 ? 's' : ''}:\n`));
|
|
139
|
+
|
|
140
|
+
// 5. Display and optionally share each connection
|
|
141
|
+
for (const match of topMatches) {
|
|
142
|
+
formatConnection({
|
|
143
|
+
villageName: match.villageName,
|
|
144
|
+
topic: match.theirTags.themes[0] || match.theirTags.track || 'general',
|
|
145
|
+
matchType: match.matchTypes.join(', '),
|
|
146
|
+
score: match.score,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const { share } = await inquirer.prompt([{
|
|
150
|
+
type: 'confirm',
|
|
151
|
+
name: 'share',
|
|
152
|
+
message: 'Share this connection with the network?',
|
|
153
|
+
default: false,
|
|
154
|
+
}]);
|
|
155
|
+
|
|
156
|
+
if (share) {
|
|
157
|
+
const description = `Connection between ${ctx.village.name} and ${match.villageName}: ${match.matchTypes.join('; ')}`;
|
|
158
|
+
const connectionTags = ['connection'];
|
|
159
|
+
if (match.theirTags.track) connectionTags.push(`track:${match.theirTags.track}`);
|
|
160
|
+
for (const t of match.theirTags.themes) connectionTags.push(`theme:${t}`);
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
await createPost({
|
|
164
|
+
communitySlug: ctx.community.slug,
|
|
165
|
+
postType: 'DISCUSSION',
|
|
166
|
+
title: `Connection: ${ctx.village.name} \u21C4 ${match.villageName}`,
|
|
167
|
+
body: description,
|
|
168
|
+
tags: connectionTags,
|
|
169
|
+
});
|
|
170
|
+
console.log(brand.green(' \u2713 Connection shared!\n'));
|
|
171
|
+
} catch (err) {
|
|
172
|
+
const msg = err.response?.data?.error || err.message;
|
|
173
|
+
console.log(chalk.red(` \u2717 Failed to share connection: ${msg}\n`));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} catch (err) {
|
|
178
|
+
spinner.fail(`Discovery failed: ${err.message}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Standalone discover command — fetches user's most recent check-in,
|
|
184
|
+
* parses its tags, then runs discoverLogic.
|
|
185
|
+
*/
|
|
186
|
+
export async function discoverCommand(options) {
|
|
187
|
+
if (!isAuthenticated()) {
|
|
188
|
+
console.log(chalk.red(" \u2717 Authentication required. Run 'myvillage login' first."));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const ctx = await resolveVillageContext();
|
|
193
|
+
if (!ctx) return;
|
|
194
|
+
|
|
195
|
+
// Fetch user's most recent check-in
|
|
196
|
+
const spinner = villageSpinner('Loading your latest check-in...').start();
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const result = await listPosts({
|
|
200
|
+
communitySlug: ctx.community.slug,
|
|
201
|
+
authorId: ctx.villagerUuid,
|
|
202
|
+
pageSize: 1,
|
|
203
|
+
sort: 'new',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const posts = result?.data || [];
|
|
207
|
+
const checkin = posts.find((p) => p.tags?.includes('checkin'));
|
|
208
|
+
|
|
209
|
+
if (!checkin) {
|
|
210
|
+
spinner.info('No recent check-in found. Run `myvillage checkin` first to share what your village is working on.');
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
spinner.succeed('Check-in loaded');
|
|
215
|
+
const myTags = parseTags(checkin.tags);
|
|
216
|
+
await discoverLogic(ctx, myTags);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
spinner.fail(`Failed to load check-in: ${err.message}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { isAuthenticated } from '../utils/auth.js';
|
|
4
|
+
import { brand } from '../utils/brand.js';
|
|
5
|
+
import { createPost } from '../utils/api.js';
|
|
6
|
+
import { resolveVillageContext } from '../utils/village-resolver.js';
|
|
7
|
+
|
|
8
|
+
const TRACKS = ['game', 'portal', 'agent', 'data', 'robot', 'general'];
|
|
9
|
+
|
|
10
|
+
export async function logCommand(text, options) {
|
|
11
|
+
if (!isAuthenticated()) {
|
|
12
|
+
console.log(chalk.red(" \u2717 Authentication required. Run 'myvillage login' first."));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ctx = await resolveVillageContext();
|
|
17
|
+
if (!ctx) return;
|
|
18
|
+
|
|
19
|
+
// Get log text
|
|
20
|
+
let logText = text;
|
|
21
|
+
if (!logText) {
|
|
22
|
+
const { input } = await inquirer.prompt([{
|
|
23
|
+
type: 'input',
|
|
24
|
+
name: 'input',
|
|
25
|
+
message: 'What did you work on?',
|
|
26
|
+
validate: (v) => v.trim().length > 0 || 'Please enter something.',
|
|
27
|
+
}]);
|
|
28
|
+
logText = input.trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Get track
|
|
32
|
+
let track = options.track;
|
|
33
|
+
if (!track) {
|
|
34
|
+
const { selected } = await inquirer.prompt([{
|
|
35
|
+
type: 'list',
|
|
36
|
+
name: 'selected',
|
|
37
|
+
message: 'Build track:',
|
|
38
|
+
choices: TRACKS,
|
|
39
|
+
}]);
|
|
40
|
+
track = selected;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Build tags and post
|
|
44
|
+
const tags = ['build-log', `track:${track}`];
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
await createPost({
|
|
48
|
+
communitySlug: ctx.community.slug,
|
|
49
|
+
postType: 'DISCUSSION',
|
|
50
|
+
body: logText,
|
|
51
|
+
tags,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
console.log(brand.green(` \u2713 Build log posted to ${ctx.village.name} [${track}]`));
|
|
55
|
+
} catch (err) {
|
|
56
|
+
const msg = err.response?.data?.error || err.message;
|
|
57
|
+
console.log(chalk.red(` \u2717 Failed to post build log: ${msg}`));
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/commands/login.js
CHANGED
|
@@ -268,6 +268,7 @@ export async function loginCommand(options) {
|
|
|
268
268
|
user_email: userInfo.email,
|
|
269
269
|
user_name: userInfo.name,
|
|
270
270
|
villager_id: userInfo.villager?.villagerId || null,
|
|
271
|
+
villager_uuid: userInfo.villager?.id || null,
|
|
271
272
|
});
|
|
272
273
|
|
|
273
274
|
spinner.succeed('Authentication complete!');
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { isAuthenticated } from '../utils/auth.js';
|
|
4
|
+
import { brand } from '../utils/brand.js';
|
|
5
|
+
import { createPost } from '../utils/api.js';
|
|
6
|
+
import { resolveVillageContext } from '../utils/village-resolver.js';
|
|
7
|
+
|
|
8
|
+
export async function storyCommand(options) {
|
|
9
|
+
if (!isAuthenticated()) {
|
|
10
|
+
console.log(chalk.red(" \u2717 Authentication required. Run 'myvillage login' first."));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ctx = await resolveVillageContext();
|
|
15
|
+
if (!ctx) return;
|
|
16
|
+
|
|
17
|
+
// Guided prompts
|
|
18
|
+
const answers = await inquirer.prompt([
|
|
19
|
+
{
|
|
20
|
+
type: 'editor',
|
|
21
|
+
name: 'story',
|
|
22
|
+
message: 'What story, memory, or piece of wisdom would you like to share?',
|
|
23
|
+
validate: (v) => v.trim().length > 0 || 'Please share something.',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
type: 'input',
|
|
27
|
+
name: 'source',
|
|
28
|
+
message: 'Who is this from? (you, a family elder, a community member)',
|
|
29
|
+
default: 'me',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: 'input',
|
|
33
|
+
name: 'connection',
|
|
34
|
+
message: 'Is this connected to something the village is building? (optional)',
|
|
35
|
+
},
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
// Build tags
|
|
39
|
+
const tags = ['knowledge-contribution'];
|
|
40
|
+
const sourceName = answers.source.trim();
|
|
41
|
+
if (sourceName && sourceName.toLowerCase() !== 'me') {
|
|
42
|
+
tags.push(`source:${sourceName}`);
|
|
43
|
+
}
|
|
44
|
+
if (answers.connection?.trim()) {
|
|
45
|
+
tags.push(`theme:${answers.connection.trim()}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Format body with attribution
|
|
49
|
+
let body = answers.story.trim();
|
|
50
|
+
if (sourceName && sourceName.toLowerCase() !== 'me') {
|
|
51
|
+
body = `"${body}"\n\n\u2014 ${sourceName}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await createPost({
|
|
56
|
+
communitySlug: ctx.community.slug,
|
|
57
|
+
postType: 'DISCUSSION',
|
|
58
|
+
title: 'Knowledge Contribution',
|
|
59
|
+
body,
|
|
60
|
+
tags,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
console.log(brand.green(` \u2713 Story shared with ${ctx.village.name}`));
|
|
64
|
+
} catch (err) {
|
|
65
|
+
const msg = err.response?.data?.error || err.message;
|
|
66
|
+
console.log(chalk.red(` \u2717 Failed to share story: ${msg}`));
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/index.js
CHANGED
|
@@ -53,6 +53,10 @@ import {
|
|
|
53
53
|
bizreqsStatusCommand,
|
|
54
54
|
bizreqsImportCommand,
|
|
55
55
|
} from './commands/bizreqs.js';
|
|
56
|
+
import { checkinCommand } from './commands/checkin.js';
|
|
57
|
+
import { discoverCommand } from './commands/discover.js';
|
|
58
|
+
import { logCommand } from './commands/log.js';
|
|
59
|
+
import { storyCommand } from './commands/story.js';
|
|
56
60
|
import {
|
|
57
61
|
soulprintInitCommand,
|
|
58
62
|
soulprintIngestCommand,
|
|
@@ -321,6 +325,30 @@ export function run() {
|
|
|
321
325
|
.description('Remove an MCP server tool from a local agent')
|
|
322
326
|
.action(agentRemoveToolCommand);
|
|
323
327
|
|
|
328
|
+
// ── Village Content Pipeline ───────────────────────────
|
|
329
|
+
|
|
330
|
+
program
|
|
331
|
+
.command('checkin')
|
|
332
|
+
.description('Check in with your village and share updates')
|
|
333
|
+
.option('--skip-discover', 'Skip the auto-discover step')
|
|
334
|
+
.action(checkinCommand);
|
|
335
|
+
|
|
336
|
+
program
|
|
337
|
+
.command('discover')
|
|
338
|
+
.description('Find connections between villages')
|
|
339
|
+
.action(discoverCommand);
|
|
340
|
+
|
|
341
|
+
program
|
|
342
|
+
.command('log [text]')
|
|
343
|
+
.description('Quick build log entry')
|
|
344
|
+
.option('-t, --track <track>', 'Build track (game, portal, agent, data, robot, general)')
|
|
345
|
+
.action(logCommand);
|
|
346
|
+
|
|
347
|
+
program
|
|
348
|
+
.command('story')
|
|
349
|
+
.description('Share a story or wisdom with the network')
|
|
350
|
+
.action(storyCommand);
|
|
351
|
+
|
|
324
352
|
// ── BizReqs: Business Requirements Pipeline ───────────
|
|
325
353
|
|
|
326
354
|
const bizreqsCmd = program
|
package/src/utils/ai.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { getConfig } from './config.js';
|
|
4
|
+
import { brand } from './brand.js';
|
|
5
|
+
|
|
6
|
+
export async function getAnthropicProvider() {
|
|
7
|
+
const { createAnthropic } = await import('@ai-sdk/anthropic');
|
|
8
|
+
const config = getConfig();
|
|
9
|
+
let apiKey = config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
|
|
10
|
+
|
|
11
|
+
if (!apiKey) {
|
|
12
|
+
console.log(chalk.yellow('\n Anthropic API key required for AI features.'));
|
|
13
|
+
console.log(brand.teal(' Get yours at: https://console.anthropic.com\n'));
|
|
14
|
+
|
|
15
|
+
const { key } = await inquirer.prompt([{
|
|
16
|
+
type: 'password',
|
|
17
|
+
name: 'key',
|
|
18
|
+
message: 'Anthropic API key:',
|
|
19
|
+
mask: '*',
|
|
20
|
+
validate: (input) => input.trim().length > 0 || 'API key is required',
|
|
21
|
+
}]);
|
|
22
|
+
|
|
23
|
+
apiKey = key.trim();
|
|
24
|
+
const { setConfig } = await import('./config.js');
|
|
25
|
+
setConfig({ anthropicApiKey: apiKey });
|
|
26
|
+
console.log(brand.green(' \u2713 API key saved to ~/.myvillage/config.json\n'));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return createAnthropic({ apiKey });
|
|
30
|
+
}
|
package/src/utils/api.js
CHANGED
|
@@ -379,6 +379,37 @@ export async function registerOAuthClient(name, appType, redirectUris) {
|
|
|
379
379
|
return response.data;
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
+
// ── Platform API Client (/api) ──────────────────────────
|
|
383
|
+
|
|
384
|
+
export function getPlatformClient() {
|
|
385
|
+
const config = getConfig();
|
|
386
|
+
return createClient(config.platformBaseUrl);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export async function getVillagerVillages(villagerUuid) {
|
|
390
|
+
const client = getPlatformClient();
|
|
391
|
+
const response = await client.get(`/villagers/${villagerUuid}/villages`);
|
|
392
|
+
return response.data;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export async function getVillagerByVillagerId(villagerId) {
|
|
396
|
+
const client = getPlatformClient();
|
|
397
|
+
const response = await client.get(`/villagers/by-villager-id/${villagerId}`);
|
|
398
|
+
return response.data;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export async function getVillageCommunities(villageUuid) {
|
|
402
|
+
const client = getNetworkClient();
|
|
403
|
+
const response = await client.get('/communities', { params: { villageId: villageUuid } });
|
|
404
|
+
return response.data;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export async function listPostsByFilters(params = {}) {
|
|
408
|
+
const client = getNetworkClient();
|
|
409
|
+
const response = await client.get('/posts', { params });
|
|
410
|
+
return response.data;
|
|
411
|
+
}
|
|
412
|
+
|
|
382
413
|
// ── BizReqs API Client (/api/bizreqs) ───────────────────
|
|
383
414
|
|
|
384
415
|
export function getBizReqsClient() {
|
package/src/utils/config.js
CHANGED
|
@@ -11,6 +11,7 @@ const DEFAULT_CONFIG = {
|
|
|
11
11
|
bizreqsBaseUrl: 'https://portal.myvillageproject.ai/api/bizreqs',
|
|
12
12
|
oauthBaseUrl: 'https://portal.myvillageproject.ai/api/oauth',
|
|
13
13
|
soulprintBaseUrl: 'https://soulprint-studio.myvillageproject.ai/api',
|
|
14
|
+
platformBaseUrl: 'https://portal.myvillageproject.ai/api',
|
|
14
15
|
clientId: 'mvos_aG_c729fuQxvvqYHOnkgTQ',
|
|
15
16
|
callbackPort: 3737,
|
|
16
17
|
anthropicApiKey: null,
|
package/src/utils/formatters.js
CHANGED
|
@@ -743,3 +743,88 @@ export function formatPagination(meta) {
|
|
|
743
743
|
console.log(chalk.dim(` ── More results available. Run with --cursor=${meta.nextCursor} to see next page\n`));
|
|
744
744
|
}
|
|
745
745
|
}
|
|
746
|
+
|
|
747
|
+
// ── MAN Content Pipeline ────────────────────────────────
|
|
748
|
+
|
|
749
|
+
export function formatCheckinSummary(post, meta) {
|
|
750
|
+
const b = (s) => chalk.hex('#B07C00')(s);
|
|
751
|
+
const width = 56;
|
|
752
|
+
const bdr = '\u2500'.repeat(width);
|
|
753
|
+
|
|
754
|
+
const lines = [''];
|
|
755
|
+
lines.push(` ${b('\u250C' + bdr + '\u2510')}`);
|
|
756
|
+
lines.push(` ${b('\u2502')} ${brand.gold('\u2743 Village Check-in')}${' '.repeat(width - 19)}${b('\u2502')}`);
|
|
757
|
+
lines.push(` ${b('\u2502' + '\u2500'.repeat(width) + '\u2502')}`);
|
|
758
|
+
|
|
759
|
+
if (meta.villageName) {
|
|
760
|
+
const vl = ` Village: ${meta.villageName}`;
|
|
761
|
+
lines.push(` ${b('\u2502')}${brand.cream(vl).padEnd(width + 12)}${b('\u2502')}`);
|
|
762
|
+
}
|
|
763
|
+
if (meta.buildTrack) {
|
|
764
|
+
const tl = ` Track: ${meta.buildTrack}`;
|
|
765
|
+
lines.push(` ${b('\u2502')}${brand.teal(tl).padEnd(width + 12)}${b('\u2502')}`);
|
|
766
|
+
}
|
|
767
|
+
if (meta.themes?.length) {
|
|
768
|
+
const th = ` Themes: ${meta.themes.join(', ')}`;
|
|
769
|
+
lines.push(` ${b('\u2502')}${chalk.white(th).padEnd(width + 12)}${b('\u2502')}`);
|
|
770
|
+
}
|
|
771
|
+
if (meta.needs?.length) {
|
|
772
|
+
const nd = ` Needs: ${meta.needs.join(', ')}`;
|
|
773
|
+
lines.push(` ${b('\u2502')}${chalk.yellow(nd).padEnd(width + 12)}${b('\u2502')}`);
|
|
774
|
+
}
|
|
775
|
+
if (meta.offers?.length) {
|
|
776
|
+
const of_ = ` Offers: ${meta.offers.join(', ')}`;
|
|
777
|
+
lines.push(` ${b('\u2502')}${brand.green(of_).padEnd(width + 12)}${b('\u2502')}`);
|
|
778
|
+
}
|
|
779
|
+
if (post?.data?.id) {
|
|
780
|
+
const id = ` Post ID: ${post.data.id.slice(0, 8)}...`;
|
|
781
|
+
lines.push(` ${b('\u2502')}${chalk.dim(id).padEnd(width + 12)}${b('\u2502')}`);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
lines.push(` ${b('\u2514' + bdr + '\u2518')}`);
|
|
785
|
+
lines.push('');
|
|
786
|
+
|
|
787
|
+
console.log(lines.join('\n'));
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
export function formatConnection(connection) {
|
|
791
|
+
const b = (s) => chalk.hex('#B07C00')(s);
|
|
792
|
+
const width = 56;
|
|
793
|
+
const bdr = '\u2500'.repeat(width);
|
|
794
|
+
|
|
795
|
+
const lines = [''];
|
|
796
|
+
lines.push(` ${b('\u250C' + bdr + '\u2510')}`);
|
|
797
|
+
lines.push(` ${b('\u2502')} ${brand.gold('\u21C4 Connection Found')}${' '.repeat(width - 20)}${b('\u2502')}`);
|
|
798
|
+
lines.push(` ${b('\u2502' + '\u2500'.repeat(width) + '\u2502')}`);
|
|
799
|
+
|
|
800
|
+
if (connection.villageName) {
|
|
801
|
+
const vl = ` Village: ${connection.villageName}`;
|
|
802
|
+
lines.push(` ${b('\u2502')}${brand.cream(vl).padEnd(width + 12)}${b('\u2502')}`);
|
|
803
|
+
}
|
|
804
|
+
if (connection.topic) {
|
|
805
|
+
const tp = ` Topic: ${connection.topic}`;
|
|
806
|
+
lines.push(` ${b('\u2502')}${chalk.white(tp).padEnd(width + 12)}${b('\u2502')}`);
|
|
807
|
+
}
|
|
808
|
+
if (connection.matchType) {
|
|
809
|
+
const mt = ` Match: ${connection.matchType}`;
|
|
810
|
+
lines.push(` ${b('\u2502')}${brand.teal(mt).padEnd(width + 12)}${b('\u2502')}`);
|
|
811
|
+
}
|
|
812
|
+
if (connection.score !== undefined) {
|
|
813
|
+
const sc = ` Relevance: ${'\u2605'.repeat(Math.min(connection.score, 5))}${'\u2606'.repeat(Math.max(0, 5 - connection.score))}`;
|
|
814
|
+
lines.push(` ${b('\u2502')}${brand.gold(sc).padEnd(width + 12)}${b('\u2502')}`);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
lines.push(` ${b('\u2514' + bdr + '\u2518')}`);
|
|
818
|
+
lines.push('');
|
|
819
|
+
|
|
820
|
+
console.log(lines.join('\n'));
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
export function formatBuildLog(post) {
|
|
824
|
+
const tags = post.tags || [];
|
|
825
|
+
const track = tags.find((t) => t.startsWith('track:'))?.replace('track:', '') || 'general';
|
|
826
|
+
const body = post.body?.length > 60 ? post.body.slice(0, 57) + '...' : post.body || '';
|
|
827
|
+
const time = relativeTime(post.createdAt);
|
|
828
|
+
|
|
829
|
+
console.log(` ${brand.teal(`[${track}]`)} ${chalk.white(body)} ${chalk.dim(time)}`);
|
|
830
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Trigger a MAN post from an automated event (deploy, publish, etc.).
|
|
5
|
+
* Stub implementation — future commands will call this to auto-generate MAN posts.
|
|
6
|
+
*
|
|
7
|
+
* @param {object} params
|
|
8
|
+
* @param {string} params.trigger - What triggered this (e.g., 'deploy', 'publish')
|
|
9
|
+
* @param {string} params.artifactId - ID of the artifact (game ID, model ID, etc.)
|
|
10
|
+
* @param {string} params.summary - Human-readable summary of what happened
|
|
11
|
+
* @param {string} params.villageId - Village UUID
|
|
12
|
+
* @param {string} params.communitySlug - Target community slug
|
|
13
|
+
*/
|
|
14
|
+
export async function triggerMANPost({ trigger, artifactId, summary, villageId, communitySlug }) {
|
|
15
|
+
console.log(chalk.dim(` [MAN hook] ${trigger}: ${summary} (not yet wired — future feature)`));
|
|
16
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { loadCredentials } from './auth.js';
|
|
4
|
+
import { brand, villageSpinner } from './brand.js';
|
|
5
|
+
import {
|
|
6
|
+
getVillagerVillages,
|
|
7
|
+
getVillagerByVillagerId,
|
|
8
|
+
getVillageCommunities,
|
|
9
|
+
joinCommunity,
|
|
10
|
+
} from './api.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolves a villager's village context: which village they're acting on behalf of
|
|
14
|
+
* and the corresponding MAN community.
|
|
15
|
+
*
|
|
16
|
+
* Returns { villagerUuid, village, community } or null if resolution fails.
|
|
17
|
+
*/
|
|
18
|
+
export async function resolveVillageContext() {
|
|
19
|
+
const creds = loadCredentials();
|
|
20
|
+
if (!creds) return null;
|
|
21
|
+
|
|
22
|
+
const spinner = villageSpinner('Resolving village context...').start();
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// 1. Get villager UUID
|
|
26
|
+
let villagerUuid = creds.villager_uuid;
|
|
27
|
+
|
|
28
|
+
if (!villagerUuid && creds.villager_id) {
|
|
29
|
+
const result = await getVillagerByVillagerId(creds.villager_id);
|
|
30
|
+
villagerUuid = result?.data?.id || result?.id;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!villagerUuid) {
|
|
34
|
+
spinner.fail('Could not resolve villager profile.');
|
|
35
|
+
console.log(chalk.dim(' Try logging out and back in: myvillage logout && myvillage login'));
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 2. Get associated villages
|
|
40
|
+
const villagesResult = await getVillagerVillages(villagerUuid);
|
|
41
|
+
const villages = villagesResult?.data || [];
|
|
42
|
+
|
|
43
|
+
if (villages.length === 0) {
|
|
44
|
+
spinner.fail('You are not associated with any villages.');
|
|
45
|
+
console.log(chalk.dim(' Join a village at portal.myvillageproject.ai to get started.'));
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
spinner.stop();
|
|
50
|
+
|
|
51
|
+
// 3. Select village
|
|
52
|
+
let village;
|
|
53
|
+
if (villages.length === 1) {
|
|
54
|
+
village = villages[0];
|
|
55
|
+
console.log(brand.teal(` Village: ${village.name}`));
|
|
56
|
+
} else {
|
|
57
|
+
const { selected } = await inquirer.prompt([{
|
|
58
|
+
type: 'list',
|
|
59
|
+
name: 'selected',
|
|
60
|
+
message: 'Which village are you representing?',
|
|
61
|
+
choices: villages.map((v) => ({
|
|
62
|
+
name: `${v.name} (${v.city}, ${v.state})`,
|
|
63
|
+
value: v,
|
|
64
|
+
})),
|
|
65
|
+
}]);
|
|
66
|
+
village = selected;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 4. Get village's dedicated community
|
|
70
|
+
const commSpinner = villageSpinner('Finding village community...').start();
|
|
71
|
+
const commResult = await getVillageCommunities(village.id);
|
|
72
|
+
const communities = commResult?.data || [];
|
|
73
|
+
|
|
74
|
+
// Find the community linked to this village
|
|
75
|
+
const community = communities.find((c) => c.villageId === village.id) || communities[0];
|
|
76
|
+
|
|
77
|
+
if (!community) {
|
|
78
|
+
commSpinner.fail('No MAN community found for this village.');
|
|
79
|
+
console.log(chalk.dim(' A community needs to be set up for your village first.'));
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 5. Auto-join community (ignore 409 = already member)
|
|
84
|
+
try {
|
|
85
|
+
await joinCommunity(community.slug);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (err.response?.status !== 409 && err.response?.status !== 400) {
|
|
88
|
+
// Only throw if it's not "already a member"
|
|
89
|
+
const msg = err.response?.data?.error || '';
|
|
90
|
+
if (!msg.toLowerCase().includes('already')) {
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
commSpinner.succeed(`Community: ${community.name}`);
|
|
97
|
+
|
|
98
|
+
return { villagerUuid, village, community };
|
|
99
|
+
} catch (err) {
|
|
100
|
+
spinner.stop();
|
|
101
|
+
console.log(chalk.red(` \u2717 Failed to resolve village context: ${err.message}`));
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|