@myvillage/cli 1.5.1 → 1.6.1
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 +2 -1
- package/src/agent-runtime/loop.js +38 -14
- package/src/commands/agent-local.js +56 -8
- package/src/commands/agent.js +6 -6
- package/src/commands/bizreqs.js +1 -25
- package/src/commands/checkin.js +116 -0
- package/src/commands/comment.js +1 -1
- package/src/commands/discover.js +220 -0
- package/src/commands/log.js +59 -0
- package/src/commands/login.js +1 -0
- package/src/commands/post.js +1 -1
- package/src/commands/story.js +68 -0
- package/src/commands/vote.js +1 -1
- package/src/index.js +28 -0
- package/src/utils/agent-scaffolder.js +5 -5
- package/src/utils/ai.js +30 -0
- package/src/utils/api.js +39 -0
- package/src/utils/config.js +6 -1
- package/src/utils/formatters.js +89 -2
- package/src/utils/man-hooks.js +16 -0
- package/src/utils/village-resolver.js +104 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myvillage/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.1",
|
|
4
4
|
"description": "MyVillageOS CLI for community developers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@ai-sdk/anthropic": "^1.0.0",
|
|
30
|
+
"@ai-sdk/openai": "^3.0.33",
|
|
30
31
|
"ai": "^4.0.0",
|
|
31
32
|
"axios": "^1.6.2",
|
|
32
33
|
"chalk": "^5.3.0",
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { generateText } from 'ai';
|
|
6
6
|
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
7
|
+
import { createOpenAI } from '@ai-sdk/openai';
|
|
7
8
|
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from 'fs';
|
|
8
9
|
import { join } from 'path';
|
|
9
10
|
import { homedir } from 'os';
|
|
@@ -18,24 +19,47 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
18
19
|
const configPath = join(agentDir, 'agent.config.yaml');
|
|
19
20
|
const config = parseYaml(readFileSync(configPath, 'utf-8'));
|
|
20
21
|
|
|
21
|
-
//
|
|
22
|
+
// Determine provider and resolve API key
|
|
23
|
+
const provider = config.brain?.provider || 'anthropic';
|
|
22
24
|
const globalConfigPath = join(homedir(), '.myvillage', 'config.json');
|
|
23
|
-
let apiKey
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
let apiKey;
|
|
26
|
+
|
|
27
|
+
if (provider === 'openai') {
|
|
28
|
+
apiKey = process.env.OPENAI_API_KEY;
|
|
29
|
+
if (!apiKey && existsSync(globalConfigPath)) {
|
|
30
|
+
try {
|
|
31
|
+
const globalConfig = JSON.parse(readFileSync(globalConfigPath, 'utf-8'));
|
|
32
|
+
apiKey = globalConfig.openaiApiKey;
|
|
33
|
+
} catch { /* ignore */ }
|
|
34
|
+
}
|
|
35
|
+
if (!apiKey) {
|
|
36
|
+
logActivity(agentDir, { type: 'error', error: 'No OpenAI API key configured' });
|
|
37
|
+
throw new Error('No OpenAI API key configured. Set OPENAI_API_KEY or run: myvillage agent start ' + agentName);
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
apiKey = process.env.ANTHROPIC_API_KEY;
|
|
41
|
+
if (!apiKey && existsSync(globalConfigPath)) {
|
|
42
|
+
try {
|
|
43
|
+
const globalConfig = JSON.parse(readFileSync(globalConfigPath, 'utf-8'));
|
|
44
|
+
apiKey = globalConfig.anthropicApiKey;
|
|
45
|
+
} catch { /* ignore */ }
|
|
46
|
+
}
|
|
47
|
+
if (!apiKey) {
|
|
48
|
+
logActivity(agentDir, { type: 'error', error: 'No Anthropic API key configured' });
|
|
49
|
+
throw new Error('No Anthropic API key configured. Set ANTHROPIC_API_KEY or run: myvillage agent start ' + agentName);
|
|
50
|
+
}
|
|
29
51
|
}
|
|
30
52
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
53
|
+
// Create provider and model
|
|
54
|
+
const modelId = config.brain?.model || (provider === 'openai' ? 'gpt-4o' : 'claude-sonnet-4-5-20250929');
|
|
55
|
+
let model;
|
|
56
|
+
if (provider === 'openai') {
|
|
57
|
+
const openai = createOpenAI({ apiKey });
|
|
58
|
+
model = openai(modelId);
|
|
59
|
+
} else {
|
|
60
|
+
const anthropic = createAnthropic({ apiKey });
|
|
61
|
+
model = anthropic(modelId);
|
|
34
62
|
}
|
|
35
|
-
|
|
36
|
-
const anthropic = createAnthropic({ apiKey });
|
|
37
|
-
const modelId = config.brain?.model || 'claude-sonnet-4-5-20250929';
|
|
38
|
-
const model = anthropic(modelId);
|
|
39
63
|
const maxTokens = config.brain?.max_tokens_per_loop || 1000;
|
|
40
64
|
|
|
41
65
|
// Initialize MCP tools
|
|
@@ -80,6 +80,35 @@ export async function agentCreateLocalCommand() {
|
|
|
80
80
|
{ name: 'Browser', value: 'browser' },
|
|
81
81
|
],
|
|
82
82
|
},
|
|
83
|
+
{
|
|
84
|
+
type: 'list',
|
|
85
|
+
name: 'provider',
|
|
86
|
+
message: 'AI provider:',
|
|
87
|
+
choices: [
|
|
88
|
+
{ name: 'Anthropic (Claude)', value: 'anthropic' },
|
|
89
|
+
{ name: 'OpenAI (GPT)', value: 'openai' },
|
|
90
|
+
],
|
|
91
|
+
default: 'anthropic',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
type: 'list',
|
|
95
|
+
name: 'model',
|
|
96
|
+
message: 'Model:',
|
|
97
|
+
choices: (prev) => {
|
|
98
|
+
if (prev.provider === 'openai') {
|
|
99
|
+
return [
|
|
100
|
+
{ name: 'gpt-4o (recommended)', value: 'gpt-4o' },
|
|
101
|
+
{ name: 'gpt-4o-mini', value: 'gpt-4o-mini' },
|
|
102
|
+
{ name: 'gpt-4.1', value: 'gpt-4.1' },
|
|
103
|
+
{ name: 'gpt-4.1-mini', value: 'gpt-4.1-mini' },
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
return [
|
|
107
|
+
{ name: 'Claude Sonnet 4.5 (recommended)', value: 'claude-sonnet-4-5-20250929' },
|
|
108
|
+
{ name: 'Claude Haiku 3.5', value: 'claude-3-5-haiku-20241022' },
|
|
109
|
+
];
|
|
110
|
+
},
|
|
111
|
+
},
|
|
83
112
|
{
|
|
84
113
|
type: 'list',
|
|
85
114
|
name: 'checkInInterval',
|
|
@@ -118,6 +147,8 @@ export async function agentCreateLocalCommand() {
|
|
|
118
147
|
description: answers.description.trim(),
|
|
119
148
|
tools,
|
|
120
149
|
checkInInterval: answers.checkInInterval,
|
|
150
|
+
provider: answers.provider,
|
|
151
|
+
model: answers.model,
|
|
121
152
|
});
|
|
122
153
|
|
|
123
154
|
spinner.succeed('Agent scaffolded!');
|
|
@@ -154,25 +185,43 @@ export async function agentStartCommand(name) {
|
|
|
154
185
|
return;
|
|
155
186
|
}
|
|
156
187
|
|
|
157
|
-
//
|
|
158
|
-
const
|
|
159
|
-
|
|
188
|
+
// Read agent's configured provider and check for the correct API key
|
|
189
|
+
const agentConfig = readAgentConfig(name);
|
|
190
|
+
const provider = agentConfig?.brain?.provider || 'anthropic';
|
|
191
|
+
const cliConfig = getConfig();
|
|
192
|
+
|
|
193
|
+
let apiKey;
|
|
194
|
+
let configKeyName;
|
|
195
|
+
let providerLabel;
|
|
196
|
+
let consoleUrl;
|
|
197
|
+
|
|
198
|
+
if (provider === 'openai') {
|
|
199
|
+
apiKey = cliConfig.openaiApiKey || process.env.OPENAI_API_KEY;
|
|
200
|
+
configKeyName = 'openaiApiKey';
|
|
201
|
+
providerLabel = 'OpenAI';
|
|
202
|
+
consoleUrl = 'https://platform.openai.com/api-keys';
|
|
203
|
+
} else {
|
|
204
|
+
apiKey = cliConfig.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
|
|
205
|
+
configKeyName = 'anthropicApiKey';
|
|
206
|
+
providerLabel = 'Anthropic';
|
|
207
|
+
consoleUrl = 'https://console.anthropic.com';
|
|
208
|
+
}
|
|
160
209
|
|
|
161
210
|
if (!apiKey) {
|
|
162
|
-
console.log(chalk.yellow(
|
|
163
|
-
console.log(brand.teal(
|
|
211
|
+
console.log(chalk.yellow(`\n ${providerLabel} API key required for this agent.`));
|
|
212
|
+
console.log(brand.teal(` Get yours at: ${consoleUrl}\n`));
|
|
164
213
|
|
|
165
214
|
try {
|
|
166
215
|
const { key } = await inquirer.prompt([{
|
|
167
216
|
type: 'password',
|
|
168
217
|
name: 'key',
|
|
169
|
-
message:
|
|
218
|
+
message: `${providerLabel} API key:`,
|
|
170
219
|
mask: '*',
|
|
171
220
|
validate: (input) => input.trim().length > 0 || 'API key is required',
|
|
172
221
|
}]);
|
|
173
222
|
|
|
174
223
|
apiKey = key.trim();
|
|
175
|
-
setConfig({
|
|
224
|
+
setConfig({ [configKeyName]: apiKey });
|
|
176
225
|
console.log(brand.green(' \u2713 API key saved.\n'));
|
|
177
226
|
} catch (err) {
|
|
178
227
|
if (err.isTtyError) {
|
|
@@ -183,7 +232,6 @@ export async function agentStartCommand(name) {
|
|
|
183
232
|
}
|
|
184
233
|
|
|
185
234
|
// Register on MAN if first start
|
|
186
|
-
const agentConfig = readAgentConfig(name);
|
|
187
235
|
if (!agentConfig.man?.agent_id) {
|
|
188
236
|
const regSpinner = villageSpinner('Registering agent on the MAN network...').start();
|
|
189
237
|
try {
|
package/src/commands/agent.js
CHANGED
|
@@ -242,7 +242,7 @@ export async function agentViewCommand(handle) {
|
|
|
242
242
|
const agent = await resolveAgentHandle(handle);
|
|
243
243
|
|
|
244
244
|
if (!agent) {
|
|
245
|
-
spinner.fail(`Agent @${handle} not found. Run 'myvillage agent' to see your agents.`);
|
|
245
|
+
spinner.fail(`Agent @${handle} not found. Run 'myvillage agent' to see your agents, or 'myvillage agent start ${handle}' if you haven't started it yet.`);
|
|
246
246
|
return;
|
|
247
247
|
}
|
|
248
248
|
|
|
@@ -271,7 +271,7 @@ export async function agentEditCommand(handle) {
|
|
|
271
271
|
const agent = await resolveAgentHandle(handle);
|
|
272
272
|
|
|
273
273
|
if (!agent) {
|
|
274
|
-
spinner.fail(`Agent @${handle} not found. Run 'myvillage agent' to see your agents.`);
|
|
274
|
+
spinner.fail(`Agent @${handle} not found. Run 'myvillage agent' to see your agents, or 'myvillage agent start ${handle}' if you haven't started it yet.`);
|
|
275
275
|
return;
|
|
276
276
|
}
|
|
277
277
|
|
|
@@ -406,7 +406,7 @@ export async function agentDeleteCommand(handle) {
|
|
|
406
406
|
const agent = await resolveAgentHandle(handle);
|
|
407
407
|
|
|
408
408
|
if (!agent) {
|
|
409
|
-
console.log(chalk.red(` \u2717 Agent @${handle} not found. Run 'myvillage agent' to see your agents.\n`));
|
|
409
|
+
console.log(chalk.red(` \u2717 Agent @${handle} not found. Run 'myvillage agent' to see your agents, or 'myvillage agent start ${handle}' if you haven't started it yet.\n`));
|
|
410
410
|
return;
|
|
411
411
|
}
|
|
412
412
|
|
|
@@ -446,7 +446,7 @@ export async function agentJoinCommand(handle, slug) {
|
|
|
446
446
|
const agent = await resolveAgentHandle(handle);
|
|
447
447
|
|
|
448
448
|
if (!agent) {
|
|
449
|
-
spinner.fail(`Agent @${handle} not found. Run 'myvillage agent' to see your agents.`);
|
|
449
|
+
spinner.fail(`Agent @${handle} not found. Run 'myvillage agent' to see your agents, or 'myvillage agent start ${handle}' if you haven't started it yet.`);
|
|
450
450
|
return;
|
|
451
451
|
}
|
|
452
452
|
|
|
@@ -470,7 +470,7 @@ export async function agentLeaveCommand(handle, slug) {
|
|
|
470
470
|
const agent = await resolveAgentHandle(handle);
|
|
471
471
|
|
|
472
472
|
if (!agent) {
|
|
473
|
-
spinner.fail(`Agent @${handle} not found. Run 'myvillage agent' to see your agents.`);
|
|
473
|
+
spinner.fail(`Agent @${handle} not found. Run 'myvillage agent' to see your agents, or 'myvillage agent start ${handle}' if you haven't started it yet.`);
|
|
474
474
|
return;
|
|
475
475
|
}
|
|
476
476
|
|
|
@@ -492,7 +492,7 @@ export async function agentRunCommand(handle, options = {}) {
|
|
|
492
492
|
const agent = await resolveAgentHandle(handle);
|
|
493
493
|
|
|
494
494
|
if (!agent) {
|
|
495
|
-
console.log(chalk.red(` \u2717 Agent @${handle} not found. Run 'myvillage agent' to see your agents.\n`));
|
|
495
|
+
console.log(chalk.red(` \u2717 Agent @${handle} not found. Run 'myvillage agent' to see your agents, or 'myvillage agent start ${handle}' if you haven't started it yet.\n`));
|
|
496
496
|
return;
|
|
497
497
|
}
|
|
498
498
|
|
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,116 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { isAuthenticated } from '../utils/auth.js';
|
|
4
|
+
import { brand, villageSpinner } from '../utils/brand.js';
|
|
5
|
+
import { createPost, structureCheckin } from '../utils/api.js';
|
|
6
|
+
import { resolveVillageContext } from '../utils/village-resolver.js';
|
|
7
|
+
import { formatCheckinSummary } from '../utils/formatters.js';
|
|
8
|
+
import { discoverLogic } from './discover.js';
|
|
9
|
+
|
|
10
|
+
export async function checkinCommand(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
|
+
console.log();
|
|
20
|
+
console.log(brand.gold(' \u2743 Village Check-in'));
|
|
21
|
+
console.log(chalk.dim(' Answer a few questions about your village. AI will help organize it.\n'));
|
|
22
|
+
|
|
23
|
+
// Conversational prompts
|
|
24
|
+
const answers = await inquirer.prompt([
|
|
25
|
+
{
|
|
26
|
+
type: 'input',
|
|
27
|
+
name: 'work',
|
|
28
|
+
message: 'What did your village work on recently?',
|
|
29
|
+
validate: (v) => v.trim().length > 0 || 'Please share what you worked on.',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
type: 'input',
|
|
33
|
+
name: 'stuck',
|
|
34
|
+
message: 'Any problems you\'re stuck on?',
|
|
35
|
+
default: '',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: 'input',
|
|
39
|
+
name: 'share',
|
|
40
|
+
message: 'Anything you want to share with the network?',
|
|
41
|
+
default: '',
|
|
42
|
+
},
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
// AI structuring (backend-proxied)
|
|
46
|
+
const spinner = villageSpinner('Organizing your check-in...').start();
|
|
47
|
+
|
|
48
|
+
let extracted;
|
|
49
|
+
try {
|
|
50
|
+
extracted = await structureCheckin({
|
|
51
|
+
work: answers.work,
|
|
52
|
+
stuck: answers.stuck || undefined,
|
|
53
|
+
share: answers.share || undefined,
|
|
54
|
+
});
|
|
55
|
+
spinner.succeed('Check-in organized');
|
|
56
|
+
} catch (err) {
|
|
57
|
+
spinner.fail('AI structuring failed \u2014 posting raw check-in');
|
|
58
|
+
// Fallback: post without AI structuring
|
|
59
|
+
extracted = {
|
|
60
|
+
themes: [],
|
|
61
|
+
needs: [],
|
|
62
|
+
offers: [],
|
|
63
|
+
buildTrack: 'general',
|
|
64
|
+
summary: [answers.work, answers.stuck, answers.share].filter(Boolean).join(' '),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Build tags
|
|
69
|
+
const tags = ['checkin'];
|
|
70
|
+
if (extracted.buildTrack) tags.push(`track:${extracted.buildTrack}`);
|
|
71
|
+
for (const t of extracted.themes) tags.push(`theme:${t}`);
|
|
72
|
+
for (const n of extracted.needs) tags.push(`need:${n}`);
|
|
73
|
+
for (const o of extracted.offers) tags.push(`offer:${o}`);
|
|
74
|
+
|
|
75
|
+
// Create post
|
|
76
|
+
try {
|
|
77
|
+
const post = await createPost({
|
|
78
|
+
communitySlug: ctx.community.slug,
|
|
79
|
+
postType: 'DISCUSSION',
|
|
80
|
+
title: `Village Check-in: ${ctx.village.name}`,
|
|
81
|
+
body: extracted.summary,
|
|
82
|
+
tags,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
formatCheckinSummary(post, {
|
|
86
|
+
villageName: ctx.village.name,
|
|
87
|
+
buildTrack: extracted.buildTrack,
|
|
88
|
+
themes: extracted.themes,
|
|
89
|
+
needs: extracted.needs,
|
|
90
|
+
offers: extracted.offers,
|
|
91
|
+
});
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const msg = err.response?.data?.error || err.message;
|
|
94
|
+
console.log(chalk.red(` \u2717 Failed to post check-in: ${msg}`));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Auto-discover prompt
|
|
99
|
+
if (!options.skipDiscover) {
|
|
100
|
+
const { runDiscover } = await inquirer.prompt([{
|
|
101
|
+
type: 'confirm',
|
|
102
|
+
name: 'runDiscover',
|
|
103
|
+
message: 'Look for connections with other villages?',
|
|
104
|
+
default: true,
|
|
105
|
+
}]);
|
|
106
|
+
|
|
107
|
+
if (runDiscover) {
|
|
108
|
+
await discoverLogic(ctx, {
|
|
109
|
+
themes: extracted.themes,
|
|
110
|
+
needs: extracted.needs,
|
|
111
|
+
offers: extracted.offers,
|
|
112
|
+
track: extracted.buildTrack,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/commands/comment.js
CHANGED
|
@@ -24,7 +24,7 @@ export async function commentCommand(postId, options) {
|
|
|
24
24
|
agentsSpinner.stop();
|
|
25
25
|
|
|
26
26
|
if (!agent) {
|
|
27
|
-
console.log(chalk.red(`
|
|
27
|
+
console.log(chalk.red(` \u2717 Agent @${options.as} not found. Run 'myvillage agent' to see your agents, or 'myvillage agent start ${options.as}' if you haven't started it yet.`));
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
30
|
agentProfileId = agent.id;
|
|
@@ -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!');
|
package/src/commands/post.js
CHANGED
|
@@ -88,7 +88,7 @@ export async function postCreateCommand(options = {}) {
|
|
|
88
88
|
agentsSpinner.stop();
|
|
89
89
|
|
|
90
90
|
if (!agent) {
|
|
91
|
-
console.log(chalk.red(`
|
|
91
|
+
console.log(chalk.red(` \u2717 Agent @${options.as} not found. Run 'myvillage agent' to see your agents, or 'myvillage agent start ${options.as}' if you haven't started it yet.`));
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
94
|
agentProfileId = agent.id;
|
|
@@ -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/commands/vote.js
CHANGED
|
@@ -20,7 +20,7 @@ export async function voteCommand(options) {
|
|
|
20
20
|
const agent = Array.isArray(agents) ? agents.find(a => a.handle === options.as) : null;
|
|
21
21
|
|
|
22
22
|
if (!agent) {
|
|
23
|
-
console.log(chalk.red(` \u2717 Agent @${options.as} not found. Run 'myvillage agent' to see your agents.\n`));
|
|
23
|
+
console.log(chalk.red(` \u2717 Agent @${options.as} not found. Run 'myvillage agent' to see your agents, or 'myvillage agent start ${options.as}' if you haven't started it yet.\n`));
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
26
|
agentProfileId = agent.id;
|
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
|
|
@@ -52,7 +52,7 @@ const INTERVAL_MAP = {
|
|
|
52
52
|
// ── Scaffolding ─────────────────────────────────────────
|
|
53
53
|
|
|
54
54
|
export function scaffoldAgent(agentDir, options) {
|
|
55
|
-
const { name, displayName, description, tools, checkInInterval } = options;
|
|
55
|
+
const { name, displayName, description, tools, checkInInterval, provider, model } = options;
|
|
56
56
|
|
|
57
57
|
// Create directories
|
|
58
58
|
mkdirSync(join(agentDir, 'routines'), { recursive: true });
|
|
@@ -61,7 +61,7 @@ export function scaffoldAgent(agentDir, options) {
|
|
|
61
61
|
// Write agent.config.yaml
|
|
62
62
|
writeFileSync(
|
|
63
63
|
join(agentDir, 'agent.config.yaml'),
|
|
64
|
-
generateAgentConfig({ name, displayName, description, checkInInterval })
|
|
64
|
+
generateAgentConfig({ name, displayName, description, checkInInterval, provider, model })
|
|
65
65
|
);
|
|
66
66
|
|
|
67
67
|
// Write prompt.md
|
|
@@ -82,7 +82,7 @@ export function scaffoldAgent(agentDir, options) {
|
|
|
82
82
|
|
|
83
83
|
// ── Config Generation ───────────────────────────────────
|
|
84
84
|
|
|
85
|
-
function generateAgentConfig({ name, displayName, description, checkInInterval }) {
|
|
85
|
+
function generateAgentConfig({ name, displayName, description, checkInInterval, provider, model }) {
|
|
86
86
|
const intervalMin = INTERVAL_MAP[checkInInterval] || 60;
|
|
87
87
|
|
|
88
88
|
const config = {
|
|
@@ -105,8 +105,8 @@ function generateAgentConfig({ name, displayName, description, checkInInterval }
|
|
|
105
105
|
},
|
|
106
106
|
|
|
107
107
|
brain: {
|
|
108
|
-
provider: 'anthropic',
|
|
109
|
-
model: 'claude-sonnet-4-5-20250929',
|
|
108
|
+
provider: provider || 'anthropic',
|
|
109
|
+
model: model || (provider === 'openai' ? 'gpt-4o' : 'claude-sonnet-4-5-20250929'),
|
|
110
110
|
max_tokens_per_loop: 1000,
|
|
111
111
|
fallback_provider: null,
|
|
112
112
|
},
|
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
|
@@ -367,6 +367,14 @@ export async function deleteAgentMemoryEntry(id, key) {
|
|
|
367
367
|
return response.data;
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
+
// ── Checkin AI Structuring ────────────────────────────────
|
|
371
|
+
|
|
372
|
+
export async function structureCheckin(data) {
|
|
373
|
+
const client = getNetworkClient();
|
|
374
|
+
const response = await client.post('/checkin/structure', data);
|
|
375
|
+
return response.data;
|
|
376
|
+
}
|
|
377
|
+
|
|
370
378
|
// ── OAuth Client Registration ────────────────────────────
|
|
371
379
|
|
|
372
380
|
export async function registerOAuthClient(name, appType, redirectUris) {
|
|
@@ -379,6 +387,37 @@ export async function registerOAuthClient(name, appType, redirectUris) {
|
|
|
379
387
|
return response.data;
|
|
380
388
|
}
|
|
381
389
|
|
|
390
|
+
// ── Platform API Client (/api) ──────────────────────────
|
|
391
|
+
|
|
392
|
+
export function getPlatformClient() {
|
|
393
|
+
const config = getConfig();
|
|
394
|
+
return createClient(config.platformBaseUrl);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export async function getVillagerVillages(villagerUuid) {
|
|
398
|
+
const client = getPlatformClient();
|
|
399
|
+
const response = await client.get(`/villagers/${villagerUuid}/villages`);
|
|
400
|
+
return response.data;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export async function getVillagerByVillagerId(villagerId) {
|
|
404
|
+
const client = getPlatformClient();
|
|
405
|
+
const response = await client.get(`/villagers/by-villager-id/${villagerId}`);
|
|
406
|
+
return response.data;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export async function getVillageCommunities(villageUuid) {
|
|
410
|
+
const client = getNetworkClient();
|
|
411
|
+
const response = await client.get('/communities', { params: { villageId: villageUuid } });
|
|
412
|
+
return response.data;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export async function listPostsByFilters(params = {}) {
|
|
416
|
+
const client = getNetworkClient();
|
|
417
|
+
const response = await client.get('/posts', { params });
|
|
418
|
+
return response.data;
|
|
419
|
+
}
|
|
420
|
+
|
|
382
421
|
// ── BizReqs API Client (/api/bizreqs) ───────────────────
|
|
383
422
|
|
|
384
423
|
export function getBizReqsClient() {
|
package/src/utils/config.js
CHANGED
|
@@ -11,9 +11,11 @@ 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,
|
|
18
|
+
openaiApiKey: null,
|
|
17
19
|
};
|
|
18
20
|
|
|
19
21
|
function ensureConfigDir() {
|
|
@@ -39,10 +41,13 @@ export function getConfig() {
|
|
|
39
41
|
delete fileConfig.clientId;
|
|
40
42
|
delete fileConfig.oauthBaseUrl;
|
|
41
43
|
const config = { ...DEFAULT_CONFIG, ...fileConfig };
|
|
42
|
-
// Environment variable
|
|
44
|
+
// Environment variable overrides for API keys
|
|
43
45
|
if (process.env.ANTHROPIC_API_KEY) {
|
|
44
46
|
config.anthropicApiKey = process.env.ANTHROPIC_API_KEY;
|
|
45
47
|
}
|
|
48
|
+
if (process.env.OPENAI_API_KEY) {
|
|
49
|
+
config.openaiApiKey = process.env.OPENAI_API_KEY;
|
|
50
|
+
}
|
|
46
51
|
return config;
|
|
47
52
|
} catch {
|
|
48
53
|
return { ...DEFAULT_CONFIG };
|
package/src/utils/formatters.js
CHANGED
|
@@ -502,10 +502,12 @@ export function formatLocalAgentStatus(agent) {
|
|
|
502
502
|
const toolsStr = Object.keys(agent.config?.tools || {}).join(', ') || 'none';
|
|
503
503
|
lines.push(` ${chalk.dim('Tools:')} ${toolsStr}`);
|
|
504
504
|
|
|
505
|
-
// Model
|
|
505
|
+
// Model & provider
|
|
506
506
|
const model = agent.config?.brain?.model;
|
|
507
507
|
if (model) {
|
|
508
|
-
|
|
508
|
+
const prov = agent.config?.brain?.provider;
|
|
509
|
+
const provLabel = prov === 'openai' ? 'OpenAI' : prov === 'anthropic' ? 'Anthropic' : prov || '';
|
|
510
|
+
lines.push(` ${chalk.dim('Model:')} ${model}${provLabel ? chalk.dim(` (${provLabel})`) : ''}`);
|
|
509
511
|
}
|
|
510
512
|
|
|
511
513
|
lines.push('');
|
|
@@ -743,3 +745,88 @@ export function formatPagination(meta) {
|
|
|
743
745
|
console.log(chalk.dim(` ── More results available. Run with --cursor=${meta.nextCursor} to see next page\n`));
|
|
744
746
|
}
|
|
745
747
|
}
|
|
748
|
+
|
|
749
|
+
// ── MAN Content Pipeline ────────────────────────────────
|
|
750
|
+
|
|
751
|
+
export function formatCheckinSummary(post, meta) {
|
|
752
|
+
const b = (s) => chalk.hex('#B07C00')(s);
|
|
753
|
+
const width = 56;
|
|
754
|
+
const bdr = '\u2500'.repeat(width);
|
|
755
|
+
|
|
756
|
+
const lines = [''];
|
|
757
|
+
lines.push(` ${b('\u250C' + bdr + '\u2510')}`);
|
|
758
|
+
lines.push(` ${b('\u2502')} ${brand.gold('\u2743 Village Check-in')}${' '.repeat(width - 19)}${b('\u2502')}`);
|
|
759
|
+
lines.push(` ${b('\u2502' + '\u2500'.repeat(width) + '\u2502')}`);
|
|
760
|
+
|
|
761
|
+
if (meta.villageName) {
|
|
762
|
+
const vl = ` Village: ${meta.villageName}`;
|
|
763
|
+
lines.push(` ${b('\u2502')}${brand.cream(vl).padEnd(width + 12)}${b('\u2502')}`);
|
|
764
|
+
}
|
|
765
|
+
if (meta.buildTrack) {
|
|
766
|
+
const tl = ` Track: ${meta.buildTrack}`;
|
|
767
|
+
lines.push(` ${b('\u2502')}${brand.teal(tl).padEnd(width + 12)}${b('\u2502')}`);
|
|
768
|
+
}
|
|
769
|
+
if (meta.themes?.length) {
|
|
770
|
+
const th = ` Themes: ${meta.themes.join(', ')}`;
|
|
771
|
+
lines.push(` ${b('\u2502')}${chalk.white(th).padEnd(width + 12)}${b('\u2502')}`);
|
|
772
|
+
}
|
|
773
|
+
if (meta.needs?.length) {
|
|
774
|
+
const nd = ` Needs: ${meta.needs.join(', ')}`;
|
|
775
|
+
lines.push(` ${b('\u2502')}${chalk.yellow(nd).padEnd(width + 12)}${b('\u2502')}`);
|
|
776
|
+
}
|
|
777
|
+
if (meta.offers?.length) {
|
|
778
|
+
const of_ = ` Offers: ${meta.offers.join(', ')}`;
|
|
779
|
+
lines.push(` ${b('\u2502')}${brand.green(of_).padEnd(width + 12)}${b('\u2502')}`);
|
|
780
|
+
}
|
|
781
|
+
if (post?.data?.id) {
|
|
782
|
+
const id = ` Post ID: ${post.data.id.slice(0, 8)}...`;
|
|
783
|
+
lines.push(` ${b('\u2502')}${chalk.dim(id).padEnd(width + 12)}${b('\u2502')}`);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
lines.push(` ${b('\u2514' + bdr + '\u2518')}`);
|
|
787
|
+
lines.push('');
|
|
788
|
+
|
|
789
|
+
console.log(lines.join('\n'));
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
export function formatConnection(connection) {
|
|
793
|
+
const b = (s) => chalk.hex('#B07C00')(s);
|
|
794
|
+
const width = 56;
|
|
795
|
+
const bdr = '\u2500'.repeat(width);
|
|
796
|
+
|
|
797
|
+
const lines = [''];
|
|
798
|
+
lines.push(` ${b('\u250C' + bdr + '\u2510')}`);
|
|
799
|
+
lines.push(` ${b('\u2502')} ${brand.gold('\u21C4 Connection Found')}${' '.repeat(width - 20)}${b('\u2502')}`);
|
|
800
|
+
lines.push(` ${b('\u2502' + '\u2500'.repeat(width) + '\u2502')}`);
|
|
801
|
+
|
|
802
|
+
if (connection.villageName) {
|
|
803
|
+
const vl = ` Village: ${connection.villageName}`;
|
|
804
|
+
lines.push(` ${b('\u2502')}${brand.cream(vl).padEnd(width + 12)}${b('\u2502')}`);
|
|
805
|
+
}
|
|
806
|
+
if (connection.topic) {
|
|
807
|
+
const tp = ` Topic: ${connection.topic}`;
|
|
808
|
+
lines.push(` ${b('\u2502')}${chalk.white(tp).padEnd(width + 12)}${b('\u2502')}`);
|
|
809
|
+
}
|
|
810
|
+
if (connection.matchType) {
|
|
811
|
+
const mt = ` Match: ${connection.matchType}`;
|
|
812
|
+
lines.push(` ${b('\u2502')}${brand.teal(mt).padEnd(width + 12)}${b('\u2502')}`);
|
|
813
|
+
}
|
|
814
|
+
if (connection.score !== undefined) {
|
|
815
|
+
const sc = ` Relevance: ${'\u2605'.repeat(Math.min(connection.score, 5))}${'\u2606'.repeat(Math.max(0, 5 - connection.score))}`;
|
|
816
|
+
lines.push(` ${b('\u2502')}${brand.gold(sc).padEnd(width + 12)}${b('\u2502')}`);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
lines.push(` ${b('\u2514' + bdr + '\u2518')}`);
|
|
820
|
+
lines.push('');
|
|
821
|
+
|
|
822
|
+
console.log(lines.join('\n'));
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
export function formatBuildLog(post) {
|
|
826
|
+
const tags = post.tags || [];
|
|
827
|
+
const track = tags.find((t) => t.startsWith('track:'))?.replace('track:', '') || 'general';
|
|
828
|
+
const body = post.body?.length > 60 ? post.body.slice(0, 57) + '...' : post.body || '';
|
|
829
|
+
const time = relativeTime(post.createdAt);
|
|
830
|
+
|
|
831
|
+
console.log(` ${brand.teal(`[${track}]`)} ${chalk.white(body)} ${chalk.dim(time)}`);
|
|
832
|
+
}
|
|
@@ -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
|
+
}
|