@myvillage/cli 1.28.1 → 1.30.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/agent-local.js +130 -0
- package/src/index.js +7 -56
- package/src/utils/agent-scaffolder.js +5 -3
- package/src/utils/api.js +0 -49
- package/src/utils/config.js +0 -1
- package/src/utils/formatters.js +0 -180
- package/src/commands/bizreqs.js +0 -941
package/src/commands/bizreqs.js
DELETED
|
@@ -1,941 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import ora from 'ora';
|
|
3
|
-
import inquirer from 'inquirer';
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
import { createInterface } from 'readline';
|
|
6
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
7
|
-
import { join, resolve } from 'path';
|
|
8
|
-
import { isAuthenticated } from '../utils/auth.js';
|
|
9
|
-
import { getConfig } from '../utils/config.js';
|
|
10
|
-
import { brand, villageSpinner, formatAIResponse } from '../utils/brand.js';
|
|
11
|
-
import {
|
|
12
|
-
createBizReqsSubmission,
|
|
13
|
-
listBizReqsSubmissions,
|
|
14
|
-
getBizReqsSubmission,
|
|
15
|
-
getBizReqsStatus,
|
|
16
|
-
saveBizReqsSpec,
|
|
17
|
-
} from '../utils/api.js';
|
|
18
|
-
import {
|
|
19
|
-
formatBizReqsList,
|
|
20
|
-
formatBizReqsStatusCounts,
|
|
21
|
-
formatBizReqsStatus,
|
|
22
|
-
formatRecommendationBox,
|
|
23
|
-
} from '../utils/formatters.js';
|
|
24
|
-
|
|
25
|
-
// ── AI System Prompt ────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
const INTAKE_SYSTEM_PROMPT = `You are a business solutions consultant for MyVillage Project, a community impact organization where student developers build real technology solutions for businesses and nonprofits.
|
|
28
|
-
|
|
29
|
-
Your job is to analyze a client's initial description and quickly arrive at a recommendation. You're warm, professional, and concise. You speak in plain language — no jargon.
|
|
30
|
-
|
|
31
|
-
## How the Conversation Works
|
|
32
|
-
|
|
33
|
-
The client has already written a description of their organization, what they do, and what challenges they face. You will receive this upfront.
|
|
34
|
-
|
|
35
|
-
1. **First response:** Briefly acknowledge what you heard (2-3 sentences max). Then, if you have enough to make a recommendation, call the ready_for_recommendation tool. If something critical is unclear, ask ONE specific follow-up question — never more than one question per response.
|
|
36
|
-
2. **Follow-up responses:** After the client answers, either call ready_for_recommendation or ask ONE more question. You should need at most 2 follow-ups. Do not ask open-ended questions — be specific about what's missing.
|
|
37
|
-
|
|
38
|
-
## Rules
|
|
39
|
-
- NEVER ask more than one question per response
|
|
40
|
-
- NEVER ask a question that bundles multiple sub-questions (e.g., "What's your budget and timeline and team size?")
|
|
41
|
-
- Prefer calling ready_for_recommendation over asking another question — only ask if truly critical info is missing
|
|
42
|
-
- Keep responses short — no long summaries or recaps unless the client asks
|
|
43
|
-
- Do not repeat back everything the client said
|
|
44
|
-
|
|
45
|
-
## MyVillageOS Capability Mapping
|
|
46
|
-
|
|
47
|
-
When thinking about recommendations, map client needs to these platform tools:
|
|
48
|
-
|
|
49
|
-
| Client Need | Platform Tool | Description |
|
|
50
|
-
|-------------|--------------|-------------|
|
|
51
|
-
| Website, dashboard, admin panel | portal | Web portal creation |
|
|
52
|
-
| Youth engagement, gamified learning | game (M-UNI) | Educational game platform |
|
|
53
|
-
| Automation, monitoring, reminders | agent | Autonomous AI agent |
|
|
54
|
-
| Community updates, social presence | feed (MAN) | Community network |
|
|
55
|
-
| Staff training, pattern recognition | model | Custom ML/AI model |
|
|
56
|
-
| Phone system, voice assistant | voice | Voice cloning + agent |
|
|
57
|
-
| STEM, hardware, robotics | robot | TechRoots robotics |
|
|
58
|
-
|
|
59
|
-
## Honesty Guidelines
|
|
60
|
-
- Be honest about what MyVillageOS can and cannot do well
|
|
61
|
-
- If something doesn't map well to the platform, say so and suggest what DOES fit
|
|
62
|
-
- Don't oversell — recommend only what genuinely serves the client's needs
|
|
63
|
-
- Most real-world needs map to a COMBINATION of tools, not just one`;
|
|
64
|
-
|
|
65
|
-
// ── Simple text input (avoids inquirer readline cursor issues) ──
|
|
66
|
-
|
|
67
|
-
function askInput(prompt) {
|
|
68
|
-
// Print styled prompt separately so readline doesn't count ANSI codes as width
|
|
69
|
-
process.stdout.write(brand.gold('? ') + chalk.bold(prompt) + ' ');
|
|
70
|
-
return new Promise((resolve) => {
|
|
71
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
72
|
-
rl.question('', (answer) => {
|
|
73
|
-
rl.close();
|
|
74
|
-
resolve(answer.trim());
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ── Tool Schemas (Zod — required by Vercel AI SDK v4) ──
|
|
80
|
-
|
|
81
|
-
const readyForRecommendationParams = z.object({
|
|
82
|
-
situation: z.string().describe('Brief summary of the organization and what they do'),
|
|
83
|
-
problems: z.string().describe('Key challenges and pain points identified'),
|
|
84
|
-
goals: z.string().optional().describe('Aspirations or goals mentioned, if any'),
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
const recommendationParams = z.object({
|
|
88
|
-
solutionName: z.string().describe('A descriptive name for the recommended solution (e.g., "NorthStar Learning Hub")'),
|
|
89
|
-
components: z.array(z.object({
|
|
90
|
-
name: z.string().describe('Component name'),
|
|
91
|
-
type: z.enum(['portal', 'game', 'agent', 'feed', 'model', 'voice', 'robot']).describe('MyVillageOS platform tool type'),
|
|
92
|
-
description: z.string().describe('What this component does for the client'),
|
|
93
|
-
complexity: z.enum(['low', 'medium', 'high']).describe('Component complexity'),
|
|
94
|
-
})).describe('Recommended solution components'),
|
|
95
|
-
estimatedTimeline: z.string().describe('Estimated build timeline (e.g., "6-8 weeks")'),
|
|
96
|
-
estimatedMVT: z.number().describe('Estimated MVT token budget'),
|
|
97
|
-
overallComplexity: z.enum(['LOW', 'MEDIUM', 'HIGH']).describe('Overall project complexity'),
|
|
98
|
-
priorityOrder: z.string().optional().describe('Recommended build order / priority for components'),
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// ── Helper: Get Anthropic client ────────────────────────
|
|
102
|
-
|
|
103
|
-
import { getAnthropicProvider } from '../utils/ai.js';
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
// ── bizreqs list ────────────────────────────────────────
|
|
107
|
-
|
|
108
|
-
export async function bizreqsListCommand(options) {
|
|
109
|
-
if (!isAuthenticated()) {
|
|
110
|
-
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const spinner = villageSpinner('Loading submissions...').start();
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
const params = {
|
|
118
|
-
limit: parseInt(options.limit) || 50,
|
|
119
|
-
offset: parseInt(options.offset) || 0,
|
|
120
|
-
counts: 'true',
|
|
121
|
-
};
|
|
122
|
-
if (options.status) params.status = options.status.toUpperCase().replace('-', '_');
|
|
123
|
-
if (options.city) params.city = options.city;
|
|
124
|
-
if (options.sort) params.sort = options.sort;
|
|
125
|
-
if (options.search) params.search = options.search;
|
|
126
|
-
|
|
127
|
-
const result = await listBizReqsSubmissions(params);
|
|
128
|
-
spinner.stop();
|
|
129
|
-
|
|
130
|
-
if (options.json) {
|
|
131
|
-
console.log(JSON.stringify(result, null, 2));
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const statusLabel = options.status
|
|
136
|
-
? ` — ${options.status.replace('-', ' ').replace(/\b\w/g, c => c.toUpperCase())}`
|
|
137
|
-
: '';
|
|
138
|
-
|
|
139
|
-
console.log(`\n ${chalk.bold(`Business Requirements Pipeline${statusLabel}`)}`);
|
|
140
|
-
formatBizReqsList(result.submissions);
|
|
141
|
-
|
|
142
|
-
if (result.statusCounts) {
|
|
143
|
-
formatBizReqsStatusCounts(result.statusCounts);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (result.pagination?.hasMore) {
|
|
147
|
-
console.log(brand.teal(` ── More results available. Use --offset=${result.pagination.offset + result.pagination.limit} to see next page\n`));
|
|
148
|
-
}
|
|
149
|
-
} catch (err) {
|
|
150
|
-
const message = err.response?.data?.error || err.message;
|
|
151
|
-
spinner.fail(`Failed to load submissions: ${message}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ── bizreqs status ──────────────────────────────────────
|
|
156
|
-
|
|
157
|
-
export async function bizreqsStatusCommand(id) {
|
|
158
|
-
if (!isAuthenticated()) {
|
|
159
|
-
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const spinner = villageSpinner(`Loading status for ${id}...`).start();
|
|
164
|
-
|
|
165
|
-
try {
|
|
166
|
-
const result = await getBizReqsStatus(id);
|
|
167
|
-
spinner.stop();
|
|
168
|
-
|
|
169
|
-
if (!result.success || !result.status) {
|
|
170
|
-
console.log(chalk.red(` ✗ Submission not found: ${id}`));
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
console.log(`\n ${chalk.bold('Project Status')}`);
|
|
175
|
-
formatBizReqsStatus(result.status);
|
|
176
|
-
} catch (err) {
|
|
177
|
-
if (err.response?.status === 404) {
|
|
178
|
-
spinner.fail(`Submission not found: ${id}`);
|
|
179
|
-
} else {
|
|
180
|
-
const message = err.response?.data?.error || err.message;
|
|
181
|
-
spinner.fail(`Failed to load status: ${message}`);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// ── bizreqs new ─────────────────────────────────────────
|
|
187
|
-
|
|
188
|
-
export async function bizreqsNewCommand(options) {
|
|
189
|
-
if (!isAuthenticated()) {
|
|
190
|
-
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
let anthropic;
|
|
195
|
-
try {
|
|
196
|
-
anthropic = await getAnthropicProvider();
|
|
197
|
-
} catch (err) {
|
|
198
|
-
console.log(chalk.red(` ✗ ${err.message}`));
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const { streamText, generateText } = await import('ai');
|
|
203
|
-
|
|
204
|
-
// Gather basic context
|
|
205
|
-
console.log(`\n ${brand.gold(chalk.bold('\u2726 MyVillage Project \u2014 Business Solutions Intake'))}\n`);
|
|
206
|
-
console.log(brand.cream(' Tell us about your organization and what you need.'));
|
|
207
|
-
console.log(brand.cream(' Our student developers will use this to build'));
|
|
208
|
-
console.log(brand.cream(' something great for you.\n'));
|
|
209
|
-
console.log(brand.teal(' Tip: Type /quit at any prompt to cancel.\n'));
|
|
210
|
-
|
|
211
|
-
let orgName = options.org;
|
|
212
|
-
let contactName = options.contact;
|
|
213
|
-
|
|
214
|
-
if (!orgName) {
|
|
215
|
-
const answer = await inquirer.prompt([{
|
|
216
|
-
type: 'input',
|
|
217
|
-
name: 'orgName',
|
|
218
|
-
message: 'Organization name:',
|
|
219
|
-
validate: v => v.trim() ? true : 'Required',
|
|
220
|
-
}]);
|
|
221
|
-
orgName = answer.orgName.trim();
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (!contactName) {
|
|
225
|
-
const answer = await inquirer.prompt([{
|
|
226
|
-
type: 'input',
|
|
227
|
-
name: 'contactName',
|
|
228
|
-
message: 'Contact name (optional):',
|
|
229
|
-
}]);
|
|
230
|
-
contactName = answer.contactName.trim() || undefined;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Read pre-context from file/URL if provided
|
|
234
|
-
let preContext = '';
|
|
235
|
-
if (options.fromFile) {
|
|
236
|
-
try {
|
|
237
|
-
preContext = readFileSync(resolve(options.fromFile), 'utf-8');
|
|
238
|
-
console.log(brand.teal(`\n 📎 Read context from: ${options.fromFile}\n`));
|
|
239
|
-
} catch (err) {
|
|
240
|
-
console.log(chalk.yellow(` ⚠ Could not read file: ${err.message}`));
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (options.fromUrl) {
|
|
245
|
-
try {
|
|
246
|
-
const { default: axios } = await import('axios');
|
|
247
|
-
const response = await axios.get(options.fromUrl, { timeout: 10000 });
|
|
248
|
-
const text = response.data
|
|
249
|
-
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
250
|
-
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
251
|
-
.replace(/<[^>]+>/g, ' ')
|
|
252
|
-
.replace(/\s+/g, ' ')
|
|
253
|
-
.trim()
|
|
254
|
-
.substring(0, 3000);
|
|
255
|
-
preContext += `\n\nWebsite content from ${options.fromUrl}:\n${text}`;
|
|
256
|
-
console.log(brand.teal(` 🌐 Scanned: ${options.fromUrl}\n`));
|
|
257
|
-
} catch (err) {
|
|
258
|
-
console.log(chalk.yellow(` ⚠ Could not fetch URL: ${err.message}`));
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Set up conversation
|
|
263
|
-
const messages = [];
|
|
264
|
-
const intakeSummary = {};
|
|
265
|
-
const maxFollowUps = options.quick ? 0 : 3;
|
|
266
|
-
|
|
267
|
-
// Collect bulk input upfront
|
|
268
|
-
console.log(brand.darkGold(' Describe your organization and the challenges you\'re facing.'));
|
|
269
|
-
console.log(brand.cream(' Include what you do, who you serve, what\'s working, what\'s not,'));
|
|
270
|
-
console.log(brand.cream(' and what you\'d love to see in the future. The more you share'));
|
|
271
|
-
console.log(brand.cream(' now, the fewer follow-up questions we\'ll need.\n'));
|
|
272
|
-
|
|
273
|
-
const bulkInput = await askInput('Your description');
|
|
274
|
-
|
|
275
|
-
if (!bulkInput) {
|
|
276
|
-
console.log(chalk.yellow('\n No input provided. Intake cancelled.\n'));
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
if (bulkInput.toLowerCase() === '/quit' || bulkInput.toLowerCase() === '/exit') {
|
|
280
|
-
console.log(brand.teal('\n Intake cancelled.\n'));
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Build initial message with all context
|
|
285
|
-
let initialMessage = `Organization: "${orgName}"`;
|
|
286
|
-
if (contactName) initialMessage += ` | Contact: ${contactName}`;
|
|
287
|
-
if (preContext) initialMessage += `\n\nPre-provided context:\n${preContext}`;
|
|
288
|
-
initialMessage += `\n\nClient's description:\n${bulkInput}`;
|
|
289
|
-
|
|
290
|
-
messages.push({ role: 'user', content: initialMessage });
|
|
291
|
-
|
|
292
|
-
// Follow-up loop: AI can ask single questions or signal ready
|
|
293
|
-
for (let i = 0; i <= maxFollowUps; i++) {
|
|
294
|
-
let fullResponse = '';
|
|
295
|
-
let toolCallMade = false;
|
|
296
|
-
|
|
297
|
-
try {
|
|
298
|
-
const result = await streamText({
|
|
299
|
-
model: anthropic('claude-sonnet-4-5-20250929'),
|
|
300
|
-
system: INTAKE_SYSTEM_PROMPT + `\n\nThe organization is "${orgName}". This is follow-up ${i} of ${maxFollowUps} max.${i === maxFollowUps ? ' You MUST call ready_for_recommendation now — no more questions.' : ''}`,
|
|
301
|
-
messages,
|
|
302
|
-
tools: {
|
|
303
|
-
ready_for_recommendation: {
|
|
304
|
-
description: 'Signal that you have enough information to generate a recommendation. Call this instead of asking another question.',
|
|
305
|
-
parameters: readyForRecommendationParams,
|
|
306
|
-
},
|
|
307
|
-
},
|
|
308
|
-
maxTokens: 512,
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
const aiSpinner = villageSpinner('Thinking...').start();
|
|
312
|
-
for await (const chunk of result.textStream) {
|
|
313
|
-
fullResponse += chunk;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const finalResult = await result;
|
|
317
|
-
aiSpinner.stop();
|
|
318
|
-
|
|
319
|
-
if (fullResponse.trim()) {
|
|
320
|
-
console.log(formatAIResponse(fullResponse.trim()));
|
|
321
|
-
messages.push({ role: 'assistant', content: fullResponse });
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (finalResult.toolCalls && finalResult.toolCalls.length > 0) {
|
|
325
|
-
for (const call of finalResult.toolCalls) {
|
|
326
|
-
if (call.toolName === 'ready_for_recommendation') {
|
|
327
|
-
toolCallMade = true;
|
|
328
|
-
intakeSummary.situation = call.args.situation;
|
|
329
|
-
intakeSummary.problems = call.args.problems;
|
|
330
|
-
intakeSummary.goals = call.args.goals || null;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
} catch (err) {
|
|
335
|
-
console.log(chalk.red(`\n AI error: ${err.message}`));
|
|
336
|
-
break;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (toolCallMade) break;
|
|
340
|
-
|
|
341
|
-
// If this was the last allowed follow-up, force move on
|
|
342
|
-
if (i === maxFollowUps) break;
|
|
343
|
-
|
|
344
|
-
// Get user response to the follow-up question
|
|
345
|
-
const userInput = await askInput('Your response');
|
|
346
|
-
|
|
347
|
-
if (!userInput) continue;
|
|
348
|
-
if (userInput.toLowerCase() === '/quit' || userInput.toLowerCase() === '/exit') {
|
|
349
|
-
console.log(brand.teal('\n Intake cancelled.\n'));
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
messages.push({ role: 'user', content: userInput });
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// Generate recommendation
|
|
357
|
-
console.log(`\n \uD83C\uDFAF ${brand.gold('Generating recommendation...')}\n`);
|
|
358
|
-
|
|
359
|
-
const recSpinner = villageSpinner('Analyzing intake data...').start();
|
|
360
|
-
|
|
361
|
-
const recPrompt = `Based on the entire conversation above about "${orgName}", generate a comprehensive solution recommendation. Consider all the problems discussed, the client's aspirations, and which MyVillageOS platform tools would best serve their needs. Call the generate_recommendation tool with your recommendation.`;
|
|
362
|
-
|
|
363
|
-
messages.push({ role: 'user', content: recPrompt });
|
|
364
|
-
|
|
365
|
-
let recommendation = null;
|
|
366
|
-
|
|
367
|
-
try {
|
|
368
|
-
const recResult = await generateText({
|
|
369
|
-
model: anthropic('claude-sonnet-4-5-20250929'),
|
|
370
|
-
system: INTAKE_SYSTEM_PROMPT,
|
|
371
|
-
messages,
|
|
372
|
-
tools: {
|
|
373
|
-
generate_recommendation: {
|
|
374
|
-
description: 'Generate a structured solution recommendation based on the intake conversation',
|
|
375
|
-
parameters: recommendationParams,
|
|
376
|
-
},
|
|
377
|
-
},
|
|
378
|
-
maxTokens: 2048,
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
if (recResult.toolCalls && recResult.toolCalls.length > 0) {
|
|
382
|
-
for (const call of recResult.toolCalls) {
|
|
383
|
-
if (call.toolName === 'generate_recommendation') {
|
|
384
|
-
recommendation = call.args;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
recSpinner.stop();
|
|
390
|
-
|
|
391
|
-
if (!recommendation) {
|
|
392
|
-
console.log(chalk.yellow(' ⚠ AI did not generate a structured recommendation. Saving transcript only.'));
|
|
393
|
-
} else {
|
|
394
|
-
formatRecommendationBox({ ...recommendation, organizationName: orgName });
|
|
395
|
-
|
|
396
|
-
// Ask for confirmation
|
|
397
|
-
const { confirmed } = await inquirer.prompt([{
|
|
398
|
-
type: 'confirm',
|
|
399
|
-
name: 'confirmed',
|
|
400
|
-
message: 'Does this recommendation look right?',
|
|
401
|
-
default: true,
|
|
402
|
-
}]);
|
|
403
|
-
|
|
404
|
-
if (!confirmed) {
|
|
405
|
-
const { adjustments } = await inquirer.prompt([{
|
|
406
|
-
type: 'input',
|
|
407
|
-
name: 'adjustments',
|
|
408
|
-
message: 'What would you like to adjust?',
|
|
409
|
-
}]);
|
|
410
|
-
|
|
411
|
-
if (adjustments.trim()) {
|
|
412
|
-
messages.push({
|
|
413
|
-
role: 'user',
|
|
414
|
-
content: `Please adjust the recommendation: ${adjustments}. Call generate_recommendation again with the updated recommendation.`,
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
const adjustSpinner = villageSpinner('Adjusting recommendation...').start();
|
|
418
|
-
|
|
419
|
-
const adjustResult = await generateText({
|
|
420
|
-
model: anthropic('claude-sonnet-4-5-20250929'),
|
|
421
|
-
system: INTAKE_SYSTEM_PROMPT,
|
|
422
|
-
messages,
|
|
423
|
-
tools: {
|
|
424
|
-
generate_recommendation: {
|
|
425
|
-
description: 'Generate a structured solution recommendation based on the intake conversation',
|
|
426
|
-
parameters: recommendationParams,
|
|
427
|
-
},
|
|
428
|
-
},
|
|
429
|
-
maxTokens: 2048,
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
adjustSpinner.stop();
|
|
433
|
-
|
|
434
|
-
if (adjustResult.toolCalls?.length > 0) {
|
|
435
|
-
for (const call of adjustResult.toolCalls) {
|
|
436
|
-
if (call.toolName === 'generate_recommendation') {
|
|
437
|
-
recommendation = call.args;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
formatRecommendationBox({ ...recommendation, organizationName: orgName });
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
} catch (err) {
|
|
446
|
-
recSpinner.fail(`AI recommendation failed: ${err.message}`);
|
|
447
|
-
console.log(brand.teal(' Saving transcript without recommendation...\n'));
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Save to backend (with local fallback)
|
|
451
|
-
const payload = {
|
|
452
|
-
organizationName: orgName,
|
|
453
|
-
contactName,
|
|
454
|
-
intakeMethod: 'CLI_INTERACTIVE',
|
|
455
|
-
conversationTranscript: messages.filter(m => m.role !== 'system'),
|
|
456
|
-
extractedSituation: intakeSummary.situation || null,
|
|
457
|
-
extractedProblems: intakeSummary.problems ? [intakeSummary.problems] : null,
|
|
458
|
-
extractedDreams: intakeSummary.goals ? [intakeSummary.goals] : null,
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
if (recommendation) {
|
|
462
|
-
payload.solutionName = recommendation.solutionName;
|
|
463
|
-
payload.components = recommendation.components;
|
|
464
|
-
payload.estimatedTimeline = recommendation.estimatedTimeline;
|
|
465
|
-
payload.estimatedMVT = recommendation.estimatedMVT;
|
|
466
|
-
payload.overallComplexity = recommendation.overallComplexity;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const saveSpinner = villageSpinner('Saving submission...').start();
|
|
470
|
-
|
|
471
|
-
try {
|
|
472
|
-
const result = await createBizReqsSubmission(payload);
|
|
473
|
-
saveSpinner.stop();
|
|
474
|
-
|
|
475
|
-
const sub = result.submission;
|
|
476
|
-
console.log(brand.green(`\n \u2713 Intake complete! Submission ID: ${chalk.bold(sub.submissionId)}\n`));
|
|
477
|
-
console.log(brand.teal(' \uD83D\uDCC4 Run \'myvillage bizreqs spec ' + sub.submissionId + '\' to generate a full project specification'));
|
|
478
|
-
console.log(brand.teal(' \uD83D\uDCCB Run \'myvillage bizreqs status ' + sub.submissionId + '\' to check status'));
|
|
479
|
-
console.log('');
|
|
480
|
-
} catch (err) {
|
|
481
|
-
saveSpinner.stop();
|
|
482
|
-
|
|
483
|
-
// Save locally so nothing is lost
|
|
484
|
-
const savedPath = saveIntakeLocally(payload);
|
|
485
|
-
const message = err.response?.data?.error || err.message;
|
|
486
|
-
console.log(chalk.yellow(`\n \u26A0 Could not reach server: ${message}`));
|
|
487
|
-
console.log(brand.green(` \u2713 Intake saved locally: ${chalk.bold(savedPath)}`));
|
|
488
|
-
console.log(brand.teal(`\n You can submit it later with:`));
|
|
489
|
-
console.log(brand.gold(` myvillage bizreqs import --file ${savedPath}\n`));
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function saveIntakeLocally(payload) {
|
|
494
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
495
|
-
const draftsDir = join(homeDir, '.myvillage', 'bizreqs-drafts');
|
|
496
|
-
if (!existsSync(draftsDir)) mkdirSync(draftsDir, { recursive: true });
|
|
497
|
-
|
|
498
|
-
const slug = payload.organizationName
|
|
499
|
-
.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
|
500
|
-
const ts = new Date().toISOString().slice(0, 10);
|
|
501
|
-
const filename = `${slug}-${ts}.json`;
|
|
502
|
-
const filepath = join(draftsDir, filename);
|
|
503
|
-
|
|
504
|
-
writeFileSync(filepath, JSON.stringify(payload, null, 2));
|
|
505
|
-
return filepath;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// ── bizreqs spec ────────────────────────────────────────
|
|
509
|
-
|
|
510
|
-
export async function bizreqsSpecCommand(id, options) {
|
|
511
|
-
if (!isAuthenticated()) {
|
|
512
|
-
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
let anthropic;
|
|
517
|
-
try {
|
|
518
|
-
anthropic = await getAnthropicProvider();
|
|
519
|
-
} catch (err) {
|
|
520
|
-
console.log(chalk.red(` ✗ ${err.message}`));
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const { generateText } = await import('ai');
|
|
525
|
-
|
|
526
|
-
// Fetch submission
|
|
527
|
-
const fetchSpinner = villageSpinner(`Loading submission ${id}...`).start();
|
|
528
|
-
let submission;
|
|
529
|
-
|
|
530
|
-
try {
|
|
531
|
-
const result = await getBizReqsSubmission(id);
|
|
532
|
-
submission = result.submission;
|
|
533
|
-
fetchSpinner.stop();
|
|
534
|
-
|
|
535
|
-
if (!submission) {
|
|
536
|
-
console.log(chalk.red(` ✗ Submission not found: ${id}`));
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
if (!submission.solutionName && !submission.components) {
|
|
541
|
-
console.log(chalk.yellow(' ⚠ This submission has no recommendation data yet.'));
|
|
542
|
-
console.log(brand.teal(' Run \'myvillage bizreqs new\' to generate a recommendation first.\n'));
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
} catch (err) {
|
|
546
|
-
if (err.response?.status === 404) {
|
|
547
|
-
fetchSpinner.fail(`Submission not found: ${id}`);
|
|
548
|
-
} else {
|
|
549
|
-
fetchSpinner.fail(`Failed to load submission: ${err.response?.data?.error || err.message}`);
|
|
550
|
-
}
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Generate spec
|
|
555
|
-
const detailLevel = options.detail || 'standard';
|
|
556
|
-
const genSpinner = villageSpinner('Generating project specification...').start();
|
|
557
|
-
|
|
558
|
-
const specPrompt = `Generate a comprehensive project specification document in Markdown format.
|
|
559
|
-
|
|
560
|
-
## Submission Details
|
|
561
|
-
|
|
562
|
-
- **Submission ID:** ${submission.submissionId}
|
|
563
|
-
- **Organization:** ${submission.organizationName}
|
|
564
|
-
- **Contact:** ${submission.contactName || 'N/A'}
|
|
565
|
-
- **City:** ${submission.city || 'N/A'}
|
|
566
|
-
- **Org Type:** ${submission.orgType || 'N/A'}
|
|
567
|
-
|
|
568
|
-
## Recommended Solution: ${submission.solutionName}
|
|
569
|
-
|
|
570
|
-
### Components
|
|
571
|
-
${JSON.stringify(submission.components, null, 2)}
|
|
572
|
-
|
|
573
|
-
### Estimates
|
|
574
|
-
- Timeline: ${submission.estimatedTimeline || 'TBD'}
|
|
575
|
-
- MVT Budget: ${submission.estimatedMVT || 'TBD'}
|
|
576
|
-
- Complexity: ${submission.overallComplexity || 'TBD'}
|
|
577
|
-
|
|
578
|
-
## Intake Context
|
|
579
|
-
${submission.extractedSituation ? `### Situation\n${submission.extractedSituation}` : ''}
|
|
580
|
-
${submission.extractedProblems ? `### Problems\n${JSON.stringify(submission.extractedProblems)}` : ''}
|
|
581
|
-
${submission.extractedDreams ? `### Dreams\n${JSON.stringify(submission.extractedDreams)}` : ''}
|
|
582
|
-
|
|
583
|
-
## Conversation Transcript
|
|
584
|
-
${submission.conversationTranscript ? JSON.stringify(submission.conversationTranscript, null, 2) : 'No transcript available'}
|
|
585
|
-
|
|
586
|
-
---
|
|
587
|
-
|
|
588
|
-
Generate the spec with the following 9 sections:
|
|
589
|
-
1. Executive Summary
|
|
590
|
-
2. Client Context
|
|
591
|
-
3. Solution Architecture
|
|
592
|
-
4. Component Specifications (one subsection per component with user stories, key screens/mechanics, acceptance criteria)
|
|
593
|
-
5. Implementation Plan (phases, sprints, milestones)
|
|
594
|
-
6. MVT Budget (allocation per component and role)
|
|
595
|
-
7. Success Metrics
|
|
596
|
-
8. Client Deliverables
|
|
597
|
-
9. Appendix
|
|
598
|
-
|
|
599
|
-
Detail level: ${detailLevel}
|
|
600
|
-
${detailLevel === 'brief' ? 'Keep it concise — 2-3 pages, high-level overview.' : ''}
|
|
601
|
-
${detailLevel === 'comprehensive' ? 'Be thorough — include wireframe descriptions, data models, and detailed user stories.' : ''}
|
|
602
|
-
|
|
603
|
-
Format as clean Markdown. Start with a YAML-style header block.`;
|
|
604
|
-
|
|
605
|
-
try {
|
|
606
|
-
const specResult = await generateText({
|
|
607
|
-
model: anthropic('claude-sonnet-4-5-20250929'),
|
|
608
|
-
system: 'You are a technical project specification writer for MyVillage Project. Generate clear, actionable project specifications that student developers can use to build real solutions. Use Markdown formatting.',
|
|
609
|
-
messages: [{ role: 'user', content: specPrompt }],
|
|
610
|
-
maxTokens: 8192,
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
genSpinner.stop();
|
|
614
|
-
|
|
615
|
-
const specContent = specResult.text;
|
|
616
|
-
|
|
617
|
-
if (!specContent || specContent.trim().length < 100) {
|
|
618
|
-
console.log(chalk.yellow(' ⚠ Spec generation produced insufficient content.'));
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// Save locally
|
|
623
|
-
const outputDir = resolve(options.output || './specs');
|
|
624
|
-
if (!existsSync(outputDir)) {
|
|
625
|
-
mkdirSync(outputDir, { recursive: true });
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
const slug = (submission.solutionName || submission.organizationName)
|
|
629
|
-
.toLowerCase()
|
|
630
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
631
|
-
.replace(/^-+|-+$/g, '')
|
|
632
|
-
.substring(0, 40);
|
|
633
|
-
|
|
634
|
-
const fileName = `${submission.submissionId}-${slug}.md`;
|
|
635
|
-
const filePath = join(outputDir, fileName);
|
|
636
|
-
writeFileSync(filePath, specContent, 'utf-8');
|
|
637
|
-
|
|
638
|
-
console.log(brand.green(`\n ✅ Specification generated!\n`));
|
|
639
|
-
console.log(` ${brand.teal('📄 Saved to:')} ${filePath}`);
|
|
640
|
-
|
|
641
|
-
// Save to backend
|
|
642
|
-
const saveSpinner = villageSpinner('Saving spec to backend...').start();
|
|
643
|
-
try {
|
|
644
|
-
await saveBizReqsSpec(submission.submissionId, {
|
|
645
|
-
specContent,
|
|
646
|
-
format: 'markdown',
|
|
647
|
-
detailLevel,
|
|
648
|
-
});
|
|
649
|
-
saveSpinner.stop();
|
|
650
|
-
console.log(brand.teal(' ☁️ Saved to MyVillageOS backend'));
|
|
651
|
-
} catch (err) {
|
|
652
|
-
saveSpinner.fail(`Could not save to backend: ${err.response?.data?.error || err.message}`);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
console.log('');
|
|
656
|
-
console.log(brand.teal(` Next: Run 'myvillage bizreqs status ${submission.submissionId}' to check status\n`));
|
|
657
|
-
} catch (err) {
|
|
658
|
-
genSpinner.fail(`Spec generation failed: ${err.message}`);
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// ── bizreqs import ──────────────────────────────────────
|
|
663
|
-
|
|
664
|
-
export async function bizreqsImportCommand(options) {
|
|
665
|
-
if (!isAuthenticated()) {
|
|
666
|
-
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
if (!options.file && !options.url) {
|
|
671
|
-
console.log(chalk.red(' ✗ Please provide --file or --url'));
|
|
672
|
-
console.log(brand.teal(' Usage: myvillage bizreqs import --file ./requirements.md'));
|
|
673
|
-
console.log(brand.teal(' myvillage bizreqs import --url https://example.org\n'));
|
|
674
|
-
return;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
let anthropic;
|
|
678
|
-
try {
|
|
679
|
-
anthropic = await getAnthropicProvider();
|
|
680
|
-
} catch (err) {
|
|
681
|
-
console.log(chalk.red(` ✗ ${err.message}`));
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
const { generateText } = await import('ai');
|
|
686
|
-
|
|
687
|
-
console.log(brand.teal('\n 📄 Importing business requirements...\n'));
|
|
688
|
-
|
|
689
|
-
// Read content
|
|
690
|
-
let content = '';
|
|
691
|
-
let sourceName = '';
|
|
692
|
-
|
|
693
|
-
if (options.file) {
|
|
694
|
-
const filePath = resolve(options.file);
|
|
695
|
-
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
696
|
-
|
|
697
|
-
if (!['txt', 'md'].includes(ext)) {
|
|
698
|
-
console.log(chalk.yellow(` ⚠ Only .txt and .md files are supported in v1. Got: .${ext}`));
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
try {
|
|
703
|
-
content = readFileSync(filePath, 'utf-8');
|
|
704
|
-
sourceName = options.file;
|
|
705
|
-
console.log(brand.teal(` 📎 Reading: ${options.file} (${content.length} chars)`));
|
|
706
|
-
} catch (err) {
|
|
707
|
-
console.log(chalk.red(` ✗ Could not read file: ${err.message}`));
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
if (options.url) {
|
|
713
|
-
try {
|
|
714
|
-
const { default: axios } = await import('axios');
|
|
715
|
-
const spinner = villageSpinner(`Fetching ${options.url}...`).start();
|
|
716
|
-
const response = await axios.get(options.url, { timeout: 15000 });
|
|
717
|
-
const text = response.data
|
|
718
|
-
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
719
|
-
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
720
|
-
.replace(/<[^>]+>/g, ' ')
|
|
721
|
-
.replace(/\s+/g, ' ')
|
|
722
|
-
.trim()
|
|
723
|
-
.substring(0, 5000);
|
|
724
|
-
content += (content ? '\n\n---\n\n' : '') + `Content from ${options.url}:\n${text}`;
|
|
725
|
-
sourceName = sourceName ? `${sourceName} + ${options.url}` : options.url;
|
|
726
|
-
spinner.stop();
|
|
727
|
-
console.log(brand.teal(` 🌐 Scanned: ${options.url}`));
|
|
728
|
-
} catch (err) {
|
|
729
|
-
console.log(chalk.yellow(` ⚠ Could not fetch URL: ${err.message}`));
|
|
730
|
-
if (!content) return;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// Prompt for org/contact
|
|
735
|
-
let orgName = options.org;
|
|
736
|
-
let contactName = options.contact;
|
|
737
|
-
|
|
738
|
-
if (!orgName) {
|
|
739
|
-
const answer = await inquirer.prompt([{
|
|
740
|
-
type: 'input',
|
|
741
|
-
name: 'orgName',
|
|
742
|
-
message: 'Organization name (or press Enter to auto-detect):',
|
|
743
|
-
}]);
|
|
744
|
-
orgName = answer.orgName.trim() || null;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
if (!contactName) {
|
|
748
|
-
const answer = await inquirer.prompt([{
|
|
749
|
-
type: 'input',
|
|
750
|
-
name: 'contactName',
|
|
751
|
-
message: 'Contact name (optional):',
|
|
752
|
-
}]);
|
|
753
|
-
contactName = answer.contactName.trim() || undefined;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// AI analysis
|
|
757
|
-
const analysisSpinner = villageSpinner('Analyzing content...').start();
|
|
758
|
-
|
|
759
|
-
try {
|
|
760
|
-
const analysisResult = await generateText({
|
|
761
|
-
model: anthropic('claude-sonnet-4-5-20250929'),
|
|
762
|
-
system: INTAKE_SYSTEM_PROMPT,
|
|
763
|
-
messages: [{
|
|
764
|
-
role: 'user',
|
|
765
|
-
content: `Analyze the following content from a potential client${orgName ? ` (${orgName})` : ''}. Extract:
|
|
766
|
-
|
|
767
|
-
1. Organization name and profile (if not provided, infer from content)
|
|
768
|
-
2. Their current situation
|
|
769
|
-
3. Problems and challenges
|
|
770
|
-
4. Any stated dreams or goals
|
|
771
|
-
5. 2-3 follow-up questions for anything unclear
|
|
772
|
-
|
|
773
|
-
Source: ${sourceName}
|
|
774
|
-
|
|
775
|
-
Content:
|
|
776
|
-
${content}
|
|
777
|
-
|
|
778
|
-
After your analysis, if you have enough information, call the generate_recommendation tool to create a solution recommendation. If you need more information first, ask your follow-up questions.`,
|
|
779
|
-
}],
|
|
780
|
-
tools: {
|
|
781
|
-
generate_recommendation: {
|
|
782
|
-
description: 'Generate a structured solution recommendation based on the intake conversation',
|
|
783
|
-
parameters: recommendationParams,
|
|
784
|
-
},
|
|
785
|
-
},
|
|
786
|
-
maxTokens: 2048,
|
|
787
|
-
});
|
|
788
|
-
|
|
789
|
-
analysisSpinner.stop();
|
|
790
|
-
|
|
791
|
-
// Display analysis text
|
|
792
|
-
if (analysisResult.text) {
|
|
793
|
-
console.log('\n' + formatAIResponse(analysisResult.text.trim()));
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
let recommendation = null;
|
|
797
|
-
|
|
798
|
-
// Check if recommendation was generated
|
|
799
|
-
if (analysisResult.toolCalls?.length > 0) {
|
|
800
|
-
for (const call of analysisResult.toolCalls) {
|
|
801
|
-
if (call.toolName === 'generate_recommendation') {
|
|
802
|
-
recommendation = call.args;
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
// If no recommendation yet, do follow-up Q&A
|
|
808
|
-
if (!recommendation) {
|
|
809
|
-
console.log('');
|
|
810
|
-
const messages = [
|
|
811
|
-
{ role: 'user', content: `Content to analyze:\n${content}` },
|
|
812
|
-
{ role: 'assistant', content: analysisResult.text || 'Analysis complete.' },
|
|
813
|
-
];
|
|
814
|
-
|
|
815
|
-
// 2-3 follow-up exchanges
|
|
816
|
-
for (let i = 0; i < 3; i++) {
|
|
817
|
-
const answer = await askInput('Your response (/done to finish)');
|
|
818
|
-
|
|
819
|
-
if (!answer || answer.toLowerCase() === '/done' || answer.toLowerCase() === '/skip') break;
|
|
820
|
-
|
|
821
|
-
messages.push({ role: 'user', content: answer });
|
|
822
|
-
|
|
823
|
-
const followUp = await generateText({
|
|
824
|
-
model: anthropic('claude-sonnet-4-5-20250929'),
|
|
825
|
-
system: INTAKE_SYSTEM_PROMPT,
|
|
826
|
-
messages,
|
|
827
|
-
tools: {
|
|
828
|
-
generate_recommendation: {
|
|
829
|
-
description: 'Generate a structured solution recommendation based on the intake conversation',
|
|
830
|
-
parameters: recommendationParams,
|
|
831
|
-
},
|
|
832
|
-
},
|
|
833
|
-
maxTokens: 1024,
|
|
834
|
-
});
|
|
835
|
-
|
|
836
|
-
if (followUp.text) {
|
|
837
|
-
console.log('\n' + formatAIResponse(followUp.text.trim()));
|
|
838
|
-
messages.push({ role: 'assistant', content: followUp.text });
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
if (followUp.toolCalls?.length > 0) {
|
|
842
|
-
for (const call of followUp.toolCalls) {
|
|
843
|
-
if (call.toolName === 'generate_recommendation') {
|
|
844
|
-
recommendation = call.args;
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
if (recommendation) break;
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
// Force recommendation if still not generated
|
|
852
|
-
if (!recommendation) {
|
|
853
|
-
const forceSpinner = villageSpinner('Generating recommendation...').start();
|
|
854
|
-
messages.push({
|
|
855
|
-
role: 'user',
|
|
856
|
-
content: 'Based on everything discussed, please call generate_recommendation with your best recommendation.',
|
|
857
|
-
});
|
|
858
|
-
|
|
859
|
-
const forceResult = await generateText({
|
|
860
|
-
model: anthropic('claude-sonnet-4-5-20250929'),
|
|
861
|
-
system: INTAKE_SYSTEM_PROMPT,
|
|
862
|
-
messages,
|
|
863
|
-
tools: {
|
|
864
|
-
generate_recommendation: {
|
|
865
|
-
description: 'Generate a structured solution recommendation based on the intake conversation',
|
|
866
|
-
parameters: recommendationParams,
|
|
867
|
-
},
|
|
868
|
-
},
|
|
869
|
-
maxTokens: 2048,
|
|
870
|
-
});
|
|
871
|
-
|
|
872
|
-
forceSpinner.stop();
|
|
873
|
-
|
|
874
|
-
if (forceResult.toolCalls?.length > 0) {
|
|
875
|
-
for (const call of forceResult.toolCalls) {
|
|
876
|
-
if (call.toolName === 'generate_recommendation') {
|
|
877
|
-
recommendation = call.args;
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
if (recommendation) {
|
|
885
|
-
if (!orgName) orgName = recommendation.organizationName || 'Unknown Organization';
|
|
886
|
-
formatRecommendationBox({ ...recommendation, organizationName: orgName });
|
|
887
|
-
|
|
888
|
-
const { confirmed } = await inquirer.prompt([{
|
|
889
|
-
type: 'confirm',
|
|
890
|
-
name: 'confirmed',
|
|
891
|
-
message: 'Save this to the pipeline?',
|
|
892
|
-
default: true,
|
|
893
|
-
}]);
|
|
894
|
-
|
|
895
|
-
if (!confirmed) {
|
|
896
|
-
console.log(brand.teal('\n Import cancelled.\n'));
|
|
897
|
-
return;
|
|
898
|
-
}
|
|
899
|
-
} else {
|
|
900
|
-
console.log(chalk.yellow('\n ⚠ Could not generate a recommendation. Saving as-is.\n'));
|
|
901
|
-
if (!orgName) {
|
|
902
|
-
const answer = await inquirer.prompt([{
|
|
903
|
-
type: 'input',
|
|
904
|
-
name: 'orgName',
|
|
905
|
-
message: 'Organization name (required):',
|
|
906
|
-
validate: v => v.trim() ? true : 'Required',
|
|
907
|
-
}]);
|
|
908
|
-
orgName = answer.orgName.trim();
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
// Save to backend
|
|
913
|
-
const saveSpinner = villageSpinner('Saving submission...').start();
|
|
914
|
-
|
|
915
|
-
const payload = {
|
|
916
|
-
organizationName: orgName,
|
|
917
|
-
contactName,
|
|
918
|
-
intakeMethod: 'CLI_IMPORT',
|
|
919
|
-
conversationTranscript: [{ role: 'system', content: `Imported from: ${sourceName}` }, { role: 'user', content }],
|
|
920
|
-
extractedSituation: analysisResult.text || null,
|
|
921
|
-
};
|
|
922
|
-
|
|
923
|
-
if (recommendation) {
|
|
924
|
-
payload.solutionName = recommendation.solutionName;
|
|
925
|
-
payload.components = recommendation.components;
|
|
926
|
-
payload.estimatedTimeline = recommendation.estimatedTimeline;
|
|
927
|
-
payload.estimatedMVT = recommendation.estimatedMVT;
|
|
928
|
-
payload.overallComplexity = recommendation.overallComplexity;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
const result = await createBizReqsSubmission(payload);
|
|
932
|
-
saveSpinner.stop();
|
|
933
|
-
|
|
934
|
-
const sub = result.submission;
|
|
935
|
-
console.log(brand.green(`\n ✅ Import complete! Submission ID: ${chalk.bold(sub.submissionId)}\n`));
|
|
936
|
-
console.log(brand.teal(' 📄 Run \'myvillage bizreqs spec ' + sub.submissionId + '\' to generate a full project specification\n'));
|
|
937
|
-
} catch (err) {
|
|
938
|
-
analysisSpinner.stop();
|
|
939
|
-
console.log(chalk.red(` ✗ Import failed: ${err.message}`));
|
|
940
|
-
}
|
|
941
|
-
}
|