wiggum-cli 0.8.0 → 0.9.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/dist/ai/conversation/conversation-manager.d.ts +11 -0
- package/dist/ai/conversation/conversation-manager.d.ts.map +1 -1
- package/dist/ai/conversation/conversation-manager.js +14 -0
- package/dist/ai/conversation/conversation-manager.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/new.d.ts +2 -0
- package/dist/commands/new.d.ts.map +1 -1
- package/dist/commands/new.js +63 -22
- package/dist/commands/new.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +97 -22
- package/dist/index.js.map +1 -1
- package/dist/tui/app.d.ts +46 -36
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +136 -37
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/components/WiggumBanner.d.ts +30 -0
- package/dist/tui/components/WiggumBanner.d.ts.map +1 -0
- package/dist/tui/components/WiggumBanner.js +34 -0
- package/dist/tui/components/WiggumBanner.js.map +1 -0
- package/dist/tui/demo.d.ts +8 -0
- package/dist/tui/demo.d.ts.map +1 -0
- package/dist/tui/demo.js +69 -0
- package/dist/tui/demo.js.map +1 -0
- package/dist/tui/hooks/useSpecGenerator.d.ts +16 -0
- package/dist/tui/hooks/useSpecGenerator.d.ts.map +1 -1
- package/dist/tui/hooks/useSpecGenerator.js +47 -0
- package/dist/tui/hooks/useSpecGenerator.js.map +1 -1
- package/dist/tui/orchestration/index.d.ts +6 -0
- package/dist/tui/orchestration/index.d.ts.map +1 -0
- package/dist/tui/orchestration/index.js +6 -0
- package/dist/tui/orchestration/index.js.map +1 -0
- package/dist/tui/orchestration/interview-orchestrator.d.ts +136 -0
- package/dist/tui/orchestration/interview-orchestrator.d.ts.map +1 -0
- package/dist/tui/orchestration/interview-orchestrator.js +437 -0
- package/dist/tui/orchestration/interview-orchestrator.js.map +1 -0
- package/dist/tui/screens/InitScreen.d.ts +26 -0
- package/dist/tui/screens/InitScreen.d.ts.map +1 -0
- package/dist/tui/screens/InitScreen.js +30 -0
- package/dist/tui/screens/InitScreen.js.map +1 -0
- package/dist/tui/screens/InterviewScreen.d.ts +2 -13
- package/dist/tui/screens/InterviewScreen.d.ts.map +1 -1
- package/dist/tui/screens/InterviewScreen.js +162 -34
- package/dist/tui/screens/InterviewScreen.js.map +1 -1
- package/dist/tui/screens/MainShell.d.ts +46 -0
- package/dist/tui/screens/MainShell.d.ts.map +1 -0
- package/dist/tui/screens/MainShell.js +196 -0
- package/dist/tui/screens/MainShell.js.map +1 -0
- package/dist/tui/screens/WelcomeScreen.d.ts +45 -0
- package/dist/tui/screens/WelcomeScreen.d.ts.map +1 -0
- package/dist/tui/screens/WelcomeScreen.js +56 -0
- package/dist/tui/screens/WelcomeScreen.js.map +1 -0
- package/dist/tui/theme.d.ts +4 -0
- package/dist/tui/theme.d.ts.map +1 -1
- package/dist/tui/theme.js +4 -0
- package/dist/tui/theme.js.map +1 -1
- package/dist/utils/repl-prompts.d.ts +1 -1
- package/dist/utils/repl-prompts.d.ts.map +1 -1
- package/dist/utils/repl-prompts.js +77 -22
- package/dist/utils/repl-prompts.js.map +1 -1
- package/package.json +1 -1
- package/src/ai/conversation/conversation-manager.ts +22 -0
- package/src/cli.ts +4 -0
- package/src/commands/new.ts +79 -27
- package/src/index.ts +109 -27
- package/src/tui/app.tsx +222 -63
- package/src/tui/components/WiggumBanner.tsx +66 -0
- package/src/tui/demo.tsx +111 -0
- package/src/tui/hooks/useSpecGenerator.ts +73 -0
- package/src/tui/orchestration/index.ts +10 -0
- package/src/tui/orchestration/interview-orchestrator.ts +559 -0
- package/src/tui/screens/InitScreen.tsx +63 -0
- package/src/tui/screens/InterviewScreen.tsx +201 -46
- package/src/tui/screens/MainShell.tsx +290 -0
- package/src/tui/screens/WelcomeScreen.tsx +141 -0
- package/src/tui/theme.ts +4 -0
- package/src/utils/repl-prompts.ts +83 -25
|
@@ -178,6 +178,28 @@ export interface UseSpecGeneratorReturn {
|
|
|
178
178
|
* Complete a tool execution
|
|
179
179
|
*/
|
|
180
180
|
completeToolCall: (toolId: string, output?: string, error?: string) => void;
|
|
181
|
+
|
|
182
|
+
// Orchestrator-specific actions
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Set the current phase (used by orchestrator)
|
|
186
|
+
*/
|
|
187
|
+
setPhase: (phase: GeneratorPhase) => void;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Set the generated spec (used by orchestrator on completion)
|
|
191
|
+
*/
|
|
192
|
+
setGeneratedSpec: (spec: string) => void;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Set an error state (used by orchestrator on error)
|
|
196
|
+
*/
|
|
197
|
+
setError: (error: string) => void;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Set working state with status message (used by orchestrator)
|
|
201
|
+
*/
|
|
202
|
+
setWorking: (isWorking: boolean, status: string) => void;
|
|
181
203
|
}
|
|
182
204
|
|
|
183
205
|
/**
|
|
@@ -571,6 +593,52 @@ export function useSpecGenerator(): UseSpecGeneratorReturn {
|
|
|
571
593
|
setState(initialState);
|
|
572
594
|
}, []);
|
|
573
595
|
|
|
596
|
+
/**
|
|
597
|
+
* Set the current phase (used by orchestrator)
|
|
598
|
+
*/
|
|
599
|
+
const setPhase = useCallback((phase: GeneratorPhase) => {
|
|
600
|
+
setState((prev) => ({
|
|
601
|
+
...prev,
|
|
602
|
+
phase,
|
|
603
|
+
}));
|
|
604
|
+
}, []);
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Set the generated spec (used by orchestrator on completion)
|
|
608
|
+
*/
|
|
609
|
+
const setGeneratedSpec = useCallback((spec: string) => {
|
|
610
|
+
setState((prev) => ({
|
|
611
|
+
...prev,
|
|
612
|
+
generatedSpec: spec,
|
|
613
|
+
phase: 'complete',
|
|
614
|
+
isWorking: false,
|
|
615
|
+
awaitingInput: false,
|
|
616
|
+
}));
|
|
617
|
+
}, []);
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Set an error state (used by orchestrator on error)
|
|
621
|
+
*/
|
|
622
|
+
const setError = useCallback((error: string) => {
|
|
623
|
+
setState((prev) => ({
|
|
624
|
+
...prev,
|
|
625
|
+
error,
|
|
626
|
+
isWorking: false,
|
|
627
|
+
}));
|
|
628
|
+
}, []);
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Set working state with status message (used by orchestrator)
|
|
632
|
+
*/
|
|
633
|
+
const setWorking = useCallback((isWorking: boolean, status: string) => {
|
|
634
|
+
setState((prev) => ({
|
|
635
|
+
...prev,
|
|
636
|
+
isWorking,
|
|
637
|
+
workingStatus: status,
|
|
638
|
+
awaitingInput: !isWorking && prev.phase !== 'complete',
|
|
639
|
+
}));
|
|
640
|
+
}, []);
|
|
641
|
+
|
|
574
642
|
return {
|
|
575
643
|
state,
|
|
576
644
|
submitAnswer,
|
|
@@ -585,5 +653,10 @@ export function useSpecGenerator(): UseSpecGeneratorReturn {
|
|
|
585
653
|
setReady,
|
|
586
654
|
startToolCall,
|
|
587
655
|
completeToolCall,
|
|
656
|
+
// Orchestrator-specific actions
|
|
657
|
+
setPhase,
|
|
658
|
+
setGeneratedSpec,
|
|
659
|
+
setError,
|
|
660
|
+
setWorking,
|
|
588
661
|
};
|
|
589
662
|
}
|
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interview Orchestrator
|
|
3
|
+
* Bridges the event-based TUI hook to ConversationManager
|
|
4
|
+
*
|
|
5
|
+
* This class manages the interview flow for spec generation,
|
|
6
|
+
* translating ConversationManager events into TUI-friendly callbacks.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ConversationManager } from '../../ai/conversation/conversation-manager.js';
|
|
10
|
+
import { fetchContent } from '../../ai/conversation/url-fetcher.js';
|
|
11
|
+
import { createInterviewTools } from '../../ai/conversation/interview-tools.js';
|
|
12
|
+
import { createTavilySearchTool, canUseTavily } from '../../ai/tools/tavily.js';
|
|
13
|
+
import { createContext7Tools, canUseContext7 } from '../../ai/tools/context7.js';
|
|
14
|
+
import type { AIProvider } from '../../ai/providers.js';
|
|
15
|
+
import type { ScanResult } from '../../scanner/types.js';
|
|
16
|
+
import type { GeneratorPhase } from '../hooks/useSpecGenerator.js';
|
|
17
|
+
|
|
18
|
+
/** Maximum number of interview questions before auto-completing */
|
|
19
|
+
const MAX_INTERVIEW_QUESTIONS = 10;
|
|
20
|
+
|
|
21
|
+
/** Minimum number of questions before AI can indicate "enough information" */
|
|
22
|
+
const MIN_INTERVIEW_QUESTIONS = 2;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Session context from /init analysis
|
|
26
|
+
*/
|
|
27
|
+
export interface SessionContext {
|
|
28
|
+
entryPoints?: string[];
|
|
29
|
+
keyDirectories?: Record<string, string>;
|
|
30
|
+
commands?: { build?: string; dev?: string; test?: string };
|
|
31
|
+
namingConventions?: string;
|
|
32
|
+
implementationGuidelines?: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Options for the InterviewOrchestrator
|
|
37
|
+
*/
|
|
38
|
+
export interface InterviewOrchestratorOptions {
|
|
39
|
+
/** Name of the feature being specified */
|
|
40
|
+
featureName: string;
|
|
41
|
+
/** Project root directory path */
|
|
42
|
+
projectRoot: string;
|
|
43
|
+
/** AI provider to use */
|
|
44
|
+
provider: AIProvider;
|
|
45
|
+
/** Model ID to use */
|
|
46
|
+
model: string;
|
|
47
|
+
/** Optional scan result with detected tech stack */
|
|
48
|
+
scanResult?: ScanResult;
|
|
49
|
+
/** Rich session context from /init */
|
|
50
|
+
sessionContext?: SessionContext;
|
|
51
|
+
/** Tavily API key for web search */
|
|
52
|
+
tavilyApiKey?: string;
|
|
53
|
+
/** Context7 API key for docs lookup */
|
|
54
|
+
context7ApiKey?: string;
|
|
55
|
+
|
|
56
|
+
// Event callbacks for TUI
|
|
57
|
+
/** Called when a message should be added to the conversation */
|
|
58
|
+
onMessage: (role: 'user' | 'assistant' | 'system', content: string) => void;
|
|
59
|
+
/** Called when streaming text should be appended */
|
|
60
|
+
onStreamChunk: (chunk: string) => void;
|
|
61
|
+
/** Called when streaming is complete */
|
|
62
|
+
onStreamComplete: () => void;
|
|
63
|
+
/** Called when a tool starts executing */
|
|
64
|
+
onToolStart: (toolName: string, input: Record<string, unknown>) => string;
|
|
65
|
+
/** Called when a tool completes */
|
|
66
|
+
onToolEnd: (toolId: string, output?: string, error?: string) => void;
|
|
67
|
+
/** Called when the phase changes */
|
|
68
|
+
onPhaseChange: (phase: GeneratorPhase) => void;
|
|
69
|
+
/** Called when spec generation is complete */
|
|
70
|
+
onComplete: (spec: string) => void;
|
|
71
|
+
/** Called when an error occurs */
|
|
72
|
+
onError: (error: string) => void;
|
|
73
|
+
/** Called when working state changes */
|
|
74
|
+
onWorkingChange: (isWorking: boolean, status: string) => void;
|
|
75
|
+
/** Called when ready for user input */
|
|
76
|
+
onReady: () => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build enhanced system prompt with project context and tool awareness
|
|
81
|
+
*/
|
|
82
|
+
function buildSystemPrompt(
|
|
83
|
+
sessionContext?: SessionContext,
|
|
84
|
+
hasTools?: { codebase: boolean; tavily: boolean; context7: boolean }
|
|
85
|
+
): string {
|
|
86
|
+
const parts: string[] = [];
|
|
87
|
+
|
|
88
|
+
// Base prompt
|
|
89
|
+
parts.push(`You are an expert product manager and technical writer helping to create detailed feature specifications.
|
|
90
|
+
|
|
91
|
+
Your role is to:
|
|
92
|
+
1. Understand the user's feature goals through targeted questions
|
|
93
|
+
2. Identify edge cases and potential issues
|
|
94
|
+
3. Generate a comprehensive, actionable specification
|
|
95
|
+
|
|
96
|
+
When interviewing:
|
|
97
|
+
- Ask one focused question at a time
|
|
98
|
+
- Acknowledge answers before asking the next question
|
|
99
|
+
- Stop asking when you have enough information (usually 3-5 questions)
|
|
100
|
+
- Say "I have enough information to generate the spec" when ready`);
|
|
101
|
+
|
|
102
|
+
// Add tool awareness
|
|
103
|
+
if (hasTools) {
|
|
104
|
+
const toolList: string[] = [];
|
|
105
|
+
if (hasTools.codebase) {
|
|
106
|
+
toolList.push('- read_file: Read project files to understand existing code');
|
|
107
|
+
toolList.push('- search_codebase: Search for patterns, functions, or imports');
|
|
108
|
+
toolList.push('- list_directory: Explore project structure');
|
|
109
|
+
}
|
|
110
|
+
if (hasTools.tavily) {
|
|
111
|
+
toolList.push('- tavily_search: Search the web for best practices and documentation');
|
|
112
|
+
}
|
|
113
|
+
if (hasTools.context7) {
|
|
114
|
+
toolList.push('- resolveLibraryId/queryDocs: Look up library documentation');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (toolList.length > 0) {
|
|
118
|
+
parts.push(`
|
|
119
|
+
## Available Tools
|
|
120
|
+
You have access to the following tools to help understand the project and gather information:
|
|
121
|
+
${toolList.join('\n')}
|
|
122
|
+
|
|
123
|
+
USE THESE TOOLS PROACTIVELY:
|
|
124
|
+
- When the user describes a feature, read relevant files to understand existing patterns
|
|
125
|
+
- When unsure about implementation, search the codebase for similar code
|
|
126
|
+
- When discussing best practices, search the web for current recommendations
|
|
127
|
+
- Don't ask the user to paste code - read it yourself`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Add emphatic web search guidance when Tavily is available
|
|
131
|
+
if (hasTools.tavily) {
|
|
132
|
+
parts.push(`
|
|
133
|
+
## IMPORTANT: Web Search Available
|
|
134
|
+
You have tavily_search to look up current best practices and documentation.
|
|
135
|
+
|
|
136
|
+
WHEN YOU MUST USE WEB SEARCH:
|
|
137
|
+
- User mentions a library, framework, or API you should verify
|
|
138
|
+
- User asks about "best practices" or "how to" patterns
|
|
139
|
+
- You need current (2026+) information not in your training data
|
|
140
|
+
- Discussing implementation approaches for modern libraries`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Add project context from /init
|
|
145
|
+
if (sessionContext) {
|
|
146
|
+
const contextParts: string[] = ['## Project Context (from analysis)'];
|
|
147
|
+
|
|
148
|
+
if (sessionContext.entryPoints && sessionContext.entryPoints.length > 0) {
|
|
149
|
+
contextParts.push(`\nEntry Points:\n${sessionContext.entryPoints.map(e => `- ${e}`).join('\n')}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (sessionContext.keyDirectories && Object.keys(sessionContext.keyDirectories).length > 0) {
|
|
153
|
+
contextParts.push(`\nKey Directories:`);
|
|
154
|
+
for (const [dir, purpose] of Object.entries(sessionContext.keyDirectories)) {
|
|
155
|
+
contextParts.push(`- ${dir}: ${purpose}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (sessionContext.commands) {
|
|
160
|
+
const cmds = sessionContext.commands;
|
|
161
|
+
const cmdList = Object.entries(cmds).filter(([_, v]) => v);
|
|
162
|
+
if (cmdList.length > 0) {
|
|
163
|
+
contextParts.push(`\nCommands:`);
|
|
164
|
+
for (const [name, cmd] of cmdList) {
|
|
165
|
+
contextParts.push(`- ${name}: ${cmd}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (contextParts.length > 1) {
|
|
171
|
+
parts.push(contextParts.join('\n'));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Spec format
|
|
176
|
+
parts.push(`
|
|
177
|
+
## Spec Format
|
|
178
|
+
When generating the spec, use this format:
|
|
179
|
+
|
|
180
|
+
# [Feature Name] Feature Specification
|
|
181
|
+
|
|
182
|
+
**Status:** Planned
|
|
183
|
+
**Version:** 1.0
|
|
184
|
+
**Last Updated:** [date]
|
|
185
|
+
|
|
186
|
+
## Purpose
|
|
187
|
+
[Brief description]
|
|
188
|
+
|
|
189
|
+
## User Stories
|
|
190
|
+
- As a [user], I want [action] so that [benefit]
|
|
191
|
+
|
|
192
|
+
## Requirements
|
|
193
|
+
|
|
194
|
+
### Functional Requirements
|
|
195
|
+
- [ ] Requirement with clear acceptance criteria
|
|
196
|
+
|
|
197
|
+
### Non-Functional Requirements
|
|
198
|
+
- [ ] Performance, security, accessibility requirements
|
|
199
|
+
|
|
200
|
+
## Technical Notes
|
|
201
|
+
- Implementation approach
|
|
202
|
+
- Key dependencies
|
|
203
|
+
- Database changes if needed
|
|
204
|
+
|
|
205
|
+
## Acceptance Criteria
|
|
206
|
+
- [ ] Specific, testable conditions
|
|
207
|
+
|
|
208
|
+
## Out of Scope
|
|
209
|
+
- Items explicitly not included`);
|
|
210
|
+
|
|
211
|
+
return parts.join('\n\n');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Extract session context from EnhancedScanResult
|
|
216
|
+
*/
|
|
217
|
+
function extractSessionContext(scanResult: ScanResult): SessionContext | undefined {
|
|
218
|
+
// Check if this is an EnhancedScanResult with aiAnalysis
|
|
219
|
+
const enhanced = scanResult as ScanResult & { aiAnalysis?: { projectContext?: SessionContext; commands?: SessionContext['commands']; implementationGuidelines?: string[] } };
|
|
220
|
+
if (!enhanced.aiAnalysis) {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const ai = enhanced.aiAnalysis;
|
|
225
|
+
return {
|
|
226
|
+
entryPoints: ai.projectContext?.entryPoints,
|
|
227
|
+
keyDirectories: ai.projectContext?.keyDirectories,
|
|
228
|
+
commands: ai.commands,
|
|
229
|
+
namingConventions: ai.projectContext?.namingConventions,
|
|
230
|
+
implementationGuidelines: ai.implementationGuidelines,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* InterviewOrchestrator
|
|
236
|
+
*
|
|
237
|
+
* Manages the interview flow for spec generation, bridging
|
|
238
|
+
* the ConversationManager to TUI callbacks.
|
|
239
|
+
*/
|
|
240
|
+
export class InterviewOrchestrator {
|
|
241
|
+
private conversation: ConversationManager;
|
|
242
|
+
private phase: GeneratorPhase = 'context';
|
|
243
|
+
private readonly featureName: string;
|
|
244
|
+
private readonly projectRoot: string;
|
|
245
|
+
private generatedSpec: string = '';
|
|
246
|
+
private questionCount: number = 0;
|
|
247
|
+
private readonly hasTools: { codebase: boolean; tavily: boolean; context7: boolean };
|
|
248
|
+
private readonly sessionContext?: SessionContext;
|
|
249
|
+
|
|
250
|
+
// Callbacks
|
|
251
|
+
private readonly onMessage: InterviewOrchestratorOptions['onMessage'];
|
|
252
|
+
private readonly onStreamChunk: InterviewOrchestratorOptions['onStreamChunk'];
|
|
253
|
+
private readonly onStreamComplete: InterviewOrchestratorOptions['onStreamComplete'];
|
|
254
|
+
private readonly onToolStart: InterviewOrchestratorOptions['onToolStart'];
|
|
255
|
+
private readonly onToolEnd: InterviewOrchestratorOptions['onToolEnd'];
|
|
256
|
+
private readonly onPhaseChange: InterviewOrchestratorOptions['onPhaseChange'];
|
|
257
|
+
private readonly onComplete: InterviewOrchestratorOptions['onComplete'];
|
|
258
|
+
private readonly onError: InterviewOrchestratorOptions['onError'];
|
|
259
|
+
private readonly onWorkingChange: InterviewOrchestratorOptions['onWorkingChange'];
|
|
260
|
+
private readonly onReady: InterviewOrchestratorOptions['onReady'];
|
|
261
|
+
|
|
262
|
+
// Track active tool calls for result mapping
|
|
263
|
+
// Uses a queue per tool name to handle multiple calls of the same tool
|
|
264
|
+
private activeToolCalls: Map<string, string[]> = new Map();
|
|
265
|
+
|
|
266
|
+
constructor(options: InterviewOrchestratorOptions) {
|
|
267
|
+
this.featureName = options.featureName;
|
|
268
|
+
this.projectRoot = options.projectRoot;
|
|
269
|
+
|
|
270
|
+
// Store callbacks
|
|
271
|
+
this.onMessage = options.onMessage;
|
|
272
|
+
this.onStreamChunk = options.onStreamChunk;
|
|
273
|
+
this.onStreamComplete = options.onStreamComplete;
|
|
274
|
+
this.onToolStart = options.onToolStart;
|
|
275
|
+
this.onToolEnd = options.onToolEnd;
|
|
276
|
+
this.onPhaseChange = options.onPhaseChange;
|
|
277
|
+
this.onComplete = options.onComplete;
|
|
278
|
+
this.onError = options.onError;
|
|
279
|
+
this.onWorkingChange = options.onWorkingChange;
|
|
280
|
+
this.onReady = options.onReady;
|
|
281
|
+
|
|
282
|
+
// Get API keys from options or environment
|
|
283
|
+
const tavilyApiKey = options.tavilyApiKey || process.env.TAVILY_API_KEY;
|
|
284
|
+
const context7ApiKey = options.context7ApiKey || process.env.CONTEXT7_API_KEY;
|
|
285
|
+
|
|
286
|
+
// Track which tools are available
|
|
287
|
+
this.hasTools = {
|
|
288
|
+
codebase: true, // Always available
|
|
289
|
+
tavily: canUseTavily(tavilyApiKey),
|
|
290
|
+
context7: canUseContext7(context7ApiKey),
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// Build tools object
|
|
294
|
+
const tools: Record<string, unknown> = {};
|
|
295
|
+
|
|
296
|
+
// Add codebase tools
|
|
297
|
+
const codebaseTools = createInterviewTools(options.projectRoot);
|
|
298
|
+
Object.assign(tools, codebaseTools);
|
|
299
|
+
|
|
300
|
+
// Add Tavily search if available
|
|
301
|
+
if (this.hasTools.tavily && tavilyApiKey) {
|
|
302
|
+
tools.tavily_search = createTavilySearchTool(tavilyApiKey);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Add Context7 tools if available
|
|
306
|
+
if (this.hasTools.context7 && context7ApiKey) {
|
|
307
|
+
const context7Tools = createContext7Tools(context7ApiKey);
|
|
308
|
+
Object.assign(tools, context7Tools);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Extract session context from scan result or use provided
|
|
312
|
+
this.sessionContext = options.sessionContext || (
|
|
313
|
+
options.scanResult ? extractSessionContext(options.scanResult) : undefined
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
// Build enhanced system prompt
|
|
317
|
+
const systemPrompt = buildSystemPrompt(this.sessionContext, this.hasTools);
|
|
318
|
+
|
|
319
|
+
// Create conversation manager with tools
|
|
320
|
+
this.conversation = new ConversationManager({
|
|
321
|
+
provider: options.provider,
|
|
322
|
+
model: options.model,
|
|
323
|
+
systemPrompt,
|
|
324
|
+
tools: tools as Record<string, never>,
|
|
325
|
+
onToolUse: (toolName, args) => {
|
|
326
|
+
// Create tool ID and notify TUI
|
|
327
|
+
const toolId = this.onToolStart(toolName, args);
|
|
328
|
+
// Store in queue for this tool name (handles multiple concurrent calls)
|
|
329
|
+
const queue = this.activeToolCalls.get(toolName) || [];
|
|
330
|
+
queue.push(toolId);
|
|
331
|
+
this.activeToolCalls.set(toolName, queue);
|
|
332
|
+
},
|
|
333
|
+
onToolResult: (toolName, result) => {
|
|
334
|
+
const queue = this.activeToolCalls.get(toolName);
|
|
335
|
+
// Get the first (oldest) tool ID from the queue (FIFO order)
|
|
336
|
+
const toolId = queue?.shift();
|
|
337
|
+
if (toolId) {
|
|
338
|
+
// Format result for display
|
|
339
|
+
const output = typeof result === 'string'
|
|
340
|
+
? result
|
|
341
|
+
: JSON.stringify(result, null, 2).slice(0, 200);
|
|
342
|
+
this.onToolEnd(toolId, output);
|
|
343
|
+
// Clean up empty queues
|
|
344
|
+
if (queue && queue.length === 0) {
|
|
345
|
+
this.activeToolCalls.delete(toolName);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
maxToolSteps: 8,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
if (options.scanResult) {
|
|
353
|
+
this.conversation.setCodebaseContext(options.scanResult);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Start the interview flow
|
|
359
|
+
* Called after the component mounts
|
|
360
|
+
*/
|
|
361
|
+
async start(): Promise<void> {
|
|
362
|
+
try {
|
|
363
|
+
// Set initial phase
|
|
364
|
+
this.onPhaseChange('context');
|
|
365
|
+
this.onMessage('system', `Spec Generator initialized for feature: ${this.featureName}`);
|
|
366
|
+
|
|
367
|
+
// Ready for context input
|
|
368
|
+
this.onReady();
|
|
369
|
+
} catch (error) {
|
|
370
|
+
this.onError(error instanceof Error ? error.message : String(error));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Add a reference URL or file path
|
|
376
|
+
*/
|
|
377
|
+
async addReference(refUrl: string): Promise<void> {
|
|
378
|
+
try {
|
|
379
|
+
this.onWorkingChange(true, 'Fetching reference...');
|
|
380
|
+
|
|
381
|
+
const result = await fetchContent(refUrl, this.projectRoot);
|
|
382
|
+
|
|
383
|
+
if (result.error) {
|
|
384
|
+
this.onMessage('system', `Error: ${result.error}`);
|
|
385
|
+
} else {
|
|
386
|
+
this.conversation.addReference(result.content, result.source);
|
|
387
|
+
const preview = result.content.slice(0, 100).replace(/\n/g, ' ').trim();
|
|
388
|
+
this.onMessage('system', `Added reference from ${result.source}: "${preview}..."`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
this.onReady();
|
|
392
|
+
} catch (error) {
|
|
393
|
+
this.onError(error instanceof Error ? error.message : String(error));
|
|
394
|
+
this.onReady();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Advance to the goals phase
|
|
400
|
+
* Called when user is done adding context
|
|
401
|
+
*/
|
|
402
|
+
async advanceToGoals(): Promise<void> {
|
|
403
|
+
this.phase = 'goals';
|
|
404
|
+
this.onPhaseChange('goals');
|
|
405
|
+
this.onMessage('system', 'Phase 2: Goals - Describe what you want to build');
|
|
406
|
+
this.onReady();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Submit user's goals and start the interview
|
|
411
|
+
*/
|
|
412
|
+
async submitGoals(goals: string): Promise<void> {
|
|
413
|
+
try {
|
|
414
|
+
this.onWorkingChange(true, 'Exploring project...');
|
|
415
|
+
|
|
416
|
+
// Add user message to conversation
|
|
417
|
+
const userMessage = goals
|
|
418
|
+
? `I want to create a feature called "${this.featureName}". Here's what I'm thinking:\n\n${goals}`
|
|
419
|
+
: `I want to create a feature called "${this.featureName}".`;
|
|
420
|
+
|
|
421
|
+
this.conversation.addToHistory({ role: 'user', content: userMessage });
|
|
422
|
+
|
|
423
|
+
// Phase 2a: Explore project silently
|
|
424
|
+
const explorePrompt = `Explore the codebase to understand the project structure for the feature "${this.featureName}".
|
|
425
|
+
Use your tools to read key files that are relevant to this feature.
|
|
426
|
+
DO NOT ask any questions yet - just gather information silently.
|
|
427
|
+
Respond with a VERY brief (1-2 sentence) summary of what you found relevant to this feature.`;
|
|
428
|
+
|
|
429
|
+
const summary = await this.conversation.chat(explorePrompt);
|
|
430
|
+
const shortSummary = summary.slice(0, 120).replace(/\n/g, ' ');
|
|
431
|
+
this.onMessage('system', `Context: ${shortSummary}${summary.length > 120 ? '...' : ''}`);
|
|
432
|
+
|
|
433
|
+
// Phase 2b: Start interview with first question
|
|
434
|
+
this.onWorkingChange(true, 'Preparing first question...');
|
|
435
|
+
|
|
436
|
+
const interviewPrompt = `Based on what you learned about the project, briefly acknowledge the user's goals for "${this.featureName}" and ask your FIRST clarifying question.
|
|
437
|
+
Ask only ONE question. Be concise.`;
|
|
438
|
+
|
|
439
|
+
const response = await this.conversation.chat(interviewPrompt);
|
|
440
|
+
this.onMessage('assistant', response);
|
|
441
|
+
|
|
442
|
+
// Transition to interview phase
|
|
443
|
+
this.phase = 'interview';
|
|
444
|
+
this.onPhaseChange('interview');
|
|
445
|
+
this.onReady();
|
|
446
|
+
} catch (error) {
|
|
447
|
+
this.onError(error instanceof Error ? error.message : String(error));
|
|
448
|
+
this.onReady();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Submit an answer during the interview phase
|
|
454
|
+
*/
|
|
455
|
+
async submitAnswer(answer: string): Promise<void> {
|
|
456
|
+
try {
|
|
457
|
+
this.onWorkingChange(true, 'Thinking...');
|
|
458
|
+
|
|
459
|
+
const response = await this.conversation.chat(answer);
|
|
460
|
+
this.onMessage('assistant', response);
|
|
461
|
+
|
|
462
|
+
this.questionCount++;
|
|
463
|
+
|
|
464
|
+
// Check if AI indicates it has enough information
|
|
465
|
+
if (this.questionCount >= MIN_INTERVIEW_QUESTIONS) {
|
|
466
|
+
const lowerResponse = response.toLowerCase();
|
|
467
|
+
if (
|
|
468
|
+
lowerResponse.includes('enough information') ||
|
|
469
|
+
lowerResponse.includes('ready to generate') ||
|
|
470
|
+
lowerResponse.includes("let me generate") ||
|
|
471
|
+
lowerResponse.includes("i'll now generate") ||
|
|
472
|
+
lowerResponse.includes("i will now generate")
|
|
473
|
+
) {
|
|
474
|
+
await this.generateSpec();
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Check if max questions reached
|
|
480
|
+
if (this.questionCount >= MAX_INTERVIEW_QUESTIONS) {
|
|
481
|
+
await this.generateSpec();
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
this.onReady();
|
|
486
|
+
} catch (error) {
|
|
487
|
+
this.onError(error instanceof Error ? error.message : String(error));
|
|
488
|
+
this.onReady();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Skip to generation phase
|
|
494
|
+
*/
|
|
495
|
+
async skipToGeneration(): Promise<void> {
|
|
496
|
+
await this.generateSpec();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Generate the specification
|
|
501
|
+
*/
|
|
502
|
+
private async generateSpec(): Promise<void> {
|
|
503
|
+
try {
|
|
504
|
+
this.phase = 'generation';
|
|
505
|
+
this.onPhaseChange('generation');
|
|
506
|
+
this.onWorkingChange(true, 'Generating specification...');
|
|
507
|
+
|
|
508
|
+
const prompt = `Based on our conversation, generate a complete feature specification for "${this.featureName}".
|
|
509
|
+
|
|
510
|
+
Use the format from your instructions. Be specific and actionable. Include:
|
|
511
|
+
- Clear user stories
|
|
512
|
+
- Detailed requirements with acceptance criteria
|
|
513
|
+
- Technical notes based on the project's tech stack
|
|
514
|
+
- Specific acceptance criteria that can be tested
|
|
515
|
+
|
|
516
|
+
Today's date is ${new Date().toISOString().split('T')[0]}.`;
|
|
517
|
+
|
|
518
|
+
// Use streaming for generation
|
|
519
|
+
const stream = this.conversation.chatStream(prompt);
|
|
520
|
+
let fullSpec = '';
|
|
521
|
+
|
|
522
|
+
for await (const chunk of stream) {
|
|
523
|
+
fullSpec += chunk;
|
|
524
|
+
this.onStreamChunk(chunk);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
this.onStreamComplete();
|
|
528
|
+
this.generatedSpec = fullSpec;
|
|
529
|
+
|
|
530
|
+
// Complete
|
|
531
|
+
this.phase = 'complete';
|
|
532
|
+
this.onPhaseChange('complete');
|
|
533
|
+
this.onComplete(fullSpec);
|
|
534
|
+
} catch (error) {
|
|
535
|
+
this.onError(error instanceof Error ? error.message : String(error));
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Get current phase
|
|
541
|
+
*/
|
|
542
|
+
getPhase(): GeneratorPhase {
|
|
543
|
+
return this.phase;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Get question count
|
|
548
|
+
*/
|
|
549
|
+
getQuestionCount(): number {
|
|
550
|
+
return this.questionCount;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Get generated spec
|
|
555
|
+
*/
|
|
556
|
+
getSpec(): string {
|
|
557
|
+
return this.generatedSpec;
|
|
558
|
+
}
|
|
559
|
+
}
|