wiggum-cli 0.7.4 → 0.7.6

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.
Files changed (39) hide show
  1. package/dist/ai/conversation/conversation-manager.d.ts +31 -1
  2. package/dist/ai/conversation/conversation-manager.d.ts.map +1 -1
  3. package/dist/ai/conversation/conversation-manager.js +48 -3
  4. package/dist/ai/conversation/conversation-manager.js.map +1 -1
  5. package/dist/ai/conversation/index.d.ts +3 -2
  6. package/dist/ai/conversation/index.d.ts.map +1 -1
  7. package/dist/ai/conversation/index.js +1 -0
  8. package/dist/ai/conversation/index.js.map +1 -1
  9. package/dist/ai/conversation/interview-tools.d.ts +85 -0
  10. package/dist/ai/conversation/interview-tools.d.ts.map +1 -0
  11. package/dist/ai/conversation/interview-tools.js +255 -0
  12. package/dist/ai/conversation/interview-tools.js.map +1 -0
  13. package/dist/ai/conversation/spec-generator.d.ts +47 -1
  14. package/dist/ai/conversation/spec-generator.d.ts.map +1 -1
  15. package/dist/ai/conversation/spec-generator.js +287 -33
  16. package/dist/ai/conversation/spec-generator.js.map +1 -1
  17. package/dist/commands/init.d.ts.map +1 -1
  18. package/dist/commands/init.js +3 -2
  19. package/dist/commands/init.js.map +1 -1
  20. package/dist/commands/new.d.ts.map +1 -1
  21. package/dist/commands/new.js +3 -0
  22. package/dist/commands/new.js.map +1 -1
  23. package/dist/utils/repl-prompts.d.ts +22 -0
  24. package/dist/utils/repl-prompts.d.ts.map +1 -1
  25. package/dist/utils/repl-prompts.js +82 -0
  26. package/dist/utils/repl-prompts.js.map +1 -1
  27. package/dist/utils/tui.d.ts +61 -0
  28. package/dist/utils/tui.d.ts.map +1 -0
  29. package/dist/utils/tui.js +214 -0
  30. package/dist/utils/tui.js.map +1 -0
  31. package/package.json +1 -1
  32. package/src/ai/conversation/conversation-manager.ts +66 -4
  33. package/src/ai/conversation/index.ts +7 -0
  34. package/src/ai/conversation/interview-tools.ts +293 -0
  35. package/src/ai/conversation/spec-generator.ts +368 -39
  36. package/src/commands/init.ts +3 -2
  37. package/src/commands/new.ts +4 -0
  38. package/src/utils/repl-prompts.ts +103 -0
  39. package/src/utils/tui.ts +262 -0
@@ -181,3 +181,106 @@ export async function confirm(options: {
181
181
  export function isCancel(value: unknown): value is null {
182
182
  return value === null;
183
183
  }
184
+
185
+ /**
186
+ * Multi-line input for paste support
187
+ * Reads input until an empty line (after content) or Ctrl+D
188
+ *
189
+ * @param prompt - The prompt to display
190
+ * @returns The collected input or null if cancelled
191
+ */
192
+ export async function multilineInput(options: {
193
+ prompt?: string;
194
+ /** Hint to show for how to end input */
195
+ endHint?: string;
196
+ }): Promise<string | null> {
197
+ const { prompt = '>', endHint = 'Press Enter twice when done' } = options;
198
+
199
+ console.log(pc.dim(` (${endHint})`));
200
+
201
+ return new Promise((resolve) => {
202
+ const lines: string[] = [];
203
+ let lastLineEmpty = false;
204
+
205
+ const rl = readline.createInterface({
206
+ input: process.stdin,
207
+ output: process.stdout,
208
+ prompt: `${pc.dim(prompt)} `,
209
+ });
210
+
211
+ rl.prompt();
212
+
213
+ rl.on('line', (line) => {
214
+ const trimmed = line.trim();
215
+
216
+ // If we get an empty line after having content, we're done
217
+ if (trimmed === '' && lines.length > 0 && lastLineEmpty) {
218
+ rl.close();
219
+ // Remove the trailing empty line we added
220
+ const result = lines.slice(0, -1).join('\n').trim();
221
+ if (result) {
222
+ console.log(pc.green('✓') + pc.dim(` Received ${result.split('\n').length} line(s)`));
223
+ }
224
+ resolve(result || null);
225
+ return;
226
+ }
227
+
228
+ lastLineEmpty = trimmed === '';
229
+ lines.push(line);
230
+ rl.prompt();
231
+ });
232
+
233
+ rl.on('close', () => {
234
+ // Ctrl+D or EOF
235
+ const result = lines.join('\n').trim();
236
+ if (result) {
237
+ console.log(pc.green('✓') + pc.dim(` Received ${result.split('\n').length} line(s)`));
238
+ }
239
+ resolve(result || null);
240
+ });
241
+
242
+ rl.on('SIGINT', () => {
243
+ rl.close();
244
+ console.log('');
245
+ resolve(null);
246
+ });
247
+ });
248
+ }
249
+
250
+ /**
251
+ * Simple text input prompt
252
+ *
253
+ * @param prompt - The prompt to display
254
+ * @returns The input text or null if cancelled
255
+ */
256
+ export async function textInput(options: {
257
+ message: string;
258
+ placeholder?: string;
259
+ }): Promise<string | null> {
260
+ const { message, placeholder } = options;
261
+
262
+ const hint = placeholder ? pc.dim(` (${placeholder})`) : '';
263
+
264
+ const rl = readline.createInterface({
265
+ input: process.stdin,
266
+ output: process.stdout,
267
+ });
268
+
269
+ return new Promise((resolve) => {
270
+ rl.question(`${simpson.yellow('?')} ${message}${hint}: `, (answer) => {
271
+ rl.close();
272
+ const trimmed = answer.trim();
273
+ if (trimmed) {
274
+ resolve(trimmed);
275
+ } else {
276
+ resolve(null);
277
+ }
278
+ });
279
+
280
+ rl.on('SIGINT', () => {
281
+ rl.close();
282
+ console.log('');
283
+ resolve(null);
284
+ });
285
+ });
286
+ }
@@ -0,0 +1,262 @@
1
+ /**
2
+ * TUI (Text User Interface) Utilities
3
+ * Claude Code-like display helpers for the interview agent
4
+ */
5
+
6
+ import pc from 'picocolors';
7
+ import { simpson } from './colors.js';
8
+
9
+ /**
10
+ * Generation phases for the spec generator
11
+ */
12
+ export type Phase = 'context' | 'goals' | 'interview' | 'generation' | 'complete';
13
+
14
+ /**
15
+ * Phase display configuration
16
+ */
17
+ interface PhaseConfig {
18
+ label: string;
19
+ description: string;
20
+ }
21
+
22
+ const PHASE_CONFIG: Record<Phase, PhaseConfig> = {
23
+ context: { label: 'Context', description: 'Share reference URLs or files' },
24
+ goals: { label: 'Goals', description: 'Describe what you want to build' },
25
+ interview: { label: 'Interview', description: 'Answer clarifying questions' },
26
+ generation: { label: 'Spec', description: 'Generate specification' },
27
+ complete: { label: 'Complete', description: 'Spec generated' },
28
+ };
29
+
30
+ /**
31
+ * Tool icons for display
32
+ */
33
+ const TOOL_ICONS: Record<string, string> = {
34
+ read_file: '📂',
35
+ search_codebase: '🔍',
36
+ list_directory: '📁',
37
+ tavily_search: '🌐',
38
+ resolveLibraryId: '📚',
39
+ queryDocs: '📚',
40
+ context7: '📚',
41
+ default: '🔧',
42
+ };
43
+
44
+ /**
45
+ * Display the phase header (Claude Code style box)
46
+ */
47
+ export function displayPhaseHeader(
48
+ featureName: string,
49
+ currentPhase: Phase,
50
+ questionCount?: { current: number; max: number }
51
+ ): void {
52
+ const width = 66;
53
+
54
+ // Build phase indicators
55
+ const phases: Phase[] = ['context', 'goals', 'interview', 'generation'];
56
+ const phaseLabels = phases.map(phase => {
57
+ const config = PHASE_CONFIG[phase];
58
+ if (phase === currentPhase) {
59
+ return `[${config.label}]`;
60
+ }
61
+ return config.label;
62
+ });
63
+
64
+ const phaseString = phaseLabels.join(' → ');
65
+
66
+ // Build right-side info (question count)
67
+ const rightInfo = questionCount
68
+ ? `Questions: ${questionCount.current}/${questionCount.max}`
69
+ : '';
70
+
71
+ // Create the header box
72
+ const title = `Feature: ${featureName}`;
73
+ const topLine = '┌─' + ' '.repeat(width - 4) + '─┐';
74
+ const bottomLine = '└─' + ' '.repeat(width - 4) + '─┘';
75
+
76
+ // Title line
77
+ const titlePadding = width - 4 - title.length;
78
+ const titleLine = `│ ${simpson.yellow(title)}${' '.repeat(Math.max(0, titlePadding))} │`;
79
+
80
+ // Phase line with right info
81
+ const phasePadding = width - 4 - phaseString.length - rightInfo.length;
82
+ const phaseLine = `│ ${pc.dim(phaseString)}${' '.repeat(Math.max(0, phasePadding))}${pc.dim(rightInfo)} │`;
83
+
84
+ console.log('');
85
+ console.log(pc.dim(topLine));
86
+ console.log(titleLine);
87
+ console.log(phaseLine);
88
+ console.log(pc.dim(bottomLine));
89
+ console.log('');
90
+ }
91
+
92
+ /**
93
+ * Display a tool usage indicator (like Claude Code)
94
+ */
95
+ export function displayToolUse(toolName: string, args: Record<string, unknown>): void {
96
+ const icon = TOOL_ICONS[toolName] || TOOL_ICONS.default;
97
+ const formattedCall = formatToolCall(toolName, args);
98
+ console.log(pc.dim(` ${icon} ${formattedCall}`));
99
+ }
100
+
101
+ /**
102
+ * Format a tool call for display
103
+ */
104
+ function formatToolCall(toolName: string, args: Record<string, unknown>): string {
105
+ switch (toolName) {
106
+ case 'read_file':
107
+ return `Reading ${args.path}`;
108
+
109
+ case 'search_codebase':
110
+ const dir = args.directory ? ` in ${args.directory}/` : '';
111
+ return `Searching for "${args.pattern}"${dir}`;
112
+
113
+ case 'list_directory':
114
+ return `Listing ${args.path || '.'}`;
115
+
116
+ case 'tavily_search':
117
+ return `Web: "${args.query}"`;
118
+
119
+ case 'resolveLibraryId':
120
+ return `Looking up library: ${args.libraryName}`;
121
+
122
+ case 'queryDocs':
123
+ return `Docs: ${args.libraryId} - "${(args.query as string)?.slice(0, 40)}..."`;
124
+
125
+ default:
126
+ // Generic format
127
+ const argStr = Object.entries(args)
128
+ .slice(0, 2)
129
+ .map(([k, v]) => `${k}=${JSON.stringify(v)}`.slice(0, 30))
130
+ .join(', ');
131
+ return `${toolName}(${argStr})`;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Display session context (project info)
137
+ */
138
+ export function displaySessionContext(context: {
139
+ projectName?: string;
140
+ stack?: string;
141
+ entryPoints?: string[];
142
+ tools?: { tavily: boolean; context7: boolean; codebase: boolean };
143
+ }): void {
144
+ const width = 66;
145
+ const topLine = '┌─' + ' '.repeat(width - 4) + '─┐';
146
+ const bottomLine = '└─' + ' '.repeat(width - 4) + '─┘';
147
+
148
+ const lines: string[] = [];
149
+
150
+ if (context.projectName) {
151
+ lines.push(`Project: ${context.projectName}`);
152
+ }
153
+ if (context.stack) {
154
+ lines.push(`Stack: ${context.stack}`);
155
+ }
156
+ if (context.entryPoints && context.entryPoints.length > 0) {
157
+ lines.push(`Entry: ${context.entryPoints.slice(0, 2).join(', ')}`);
158
+ }
159
+ if (context.tools) {
160
+ const toolList = [];
161
+ if (context.tools.tavily) toolList.push('Tavily ✓');
162
+ if (context.tools.context7) toolList.push('Context7 ✓');
163
+ if (context.tools.codebase) toolList.push('Codebase ✓');
164
+ if (toolList.length > 0) {
165
+ lines.push(`Tools: ${toolList.join(' ')}`);
166
+ }
167
+ }
168
+
169
+ if (lines.length === 0) return;
170
+
171
+ console.log(pc.dim(topLine));
172
+ for (const line of lines) {
173
+ const padding = width - 4 - line.length;
174
+ console.log(`│ ${pc.dim(line)}${' '.repeat(Math.max(0, padding))} │`);
175
+ }
176
+ console.log(pc.dim(bottomLine));
177
+ console.log('');
178
+ }
179
+
180
+ /**
181
+ * Display progress phases with visual indicators
182
+ */
183
+ export function displayProgressPhases(currentPhase: Phase): void {
184
+ const phases: Phase[] = ['context', 'goals', 'interview', 'generation'];
185
+
186
+ console.log('');
187
+ for (const phase of phases) {
188
+ const config = PHASE_CONFIG[phase];
189
+ let indicator: string;
190
+ let style: (text: string) => string;
191
+
192
+ if (phase === currentPhase) {
193
+ indicator = '◐'; // In progress
194
+ style = simpson.yellow;
195
+ } else if (getPhaseIndex(phase) < getPhaseIndex(currentPhase)) {
196
+ indicator = '●'; // Completed
197
+ style = pc.green;
198
+ } else {
199
+ indicator = '○'; // Pending
200
+ style = pc.dim;
201
+ }
202
+
203
+ const current = phase === currentPhase ? ' ← current' : '';
204
+ console.log(` ${style(indicator)} ${style(config.label)} ${pc.dim(`- ${config.description}`)}${pc.dim(current)}`);
205
+ }
206
+ console.log('');
207
+ }
208
+
209
+ /**
210
+ * Get the index of a phase for comparison
211
+ */
212
+ function getPhaseIndex(phase: Phase): number {
213
+ const order: Phase[] = ['context', 'goals', 'interview', 'generation', 'complete'];
214
+ return order.indexOf(phase);
215
+ }
216
+
217
+ /**
218
+ * Display a warning for garbled input
219
+ */
220
+ export function displayGarbledInputWarning(received: string): void {
221
+ console.log('');
222
+ console.log(pc.yellow(`⚠️ Received incomplete input: "${received.slice(0, 50)}${received.length > 50 ? '...' : ''}"`));
223
+ console.log(pc.dim(' Please paste again or type your answer.'));
224
+ }
225
+
226
+ /**
227
+ * Display AI prefix before streaming
228
+ */
229
+ export function displayAIPrefix(): void {
230
+ process.stdout.write(simpson.blue('AI: '));
231
+ }
232
+
233
+ /**
234
+ * Display a simple separator line
235
+ */
236
+ export function displaySeparator(): void {
237
+ console.log(pc.dim('─'.repeat(60)));
238
+ }
239
+
240
+ /**
241
+ * Display thinking indicator
242
+ */
243
+ export function displayThinking(): void {
244
+ process.stdout.write(pc.dim('Thinking...'));
245
+ }
246
+
247
+ /**
248
+ * Clear thinking indicator
249
+ */
250
+ export function clearThinking(): void {
251
+ // Move cursor back and clear line
252
+ process.stdout.write('\r' + ' '.repeat(20) + '\r');
253
+ }
254
+
255
+ /**
256
+ * Format bytes for display
257
+ */
258
+ export function formatBytes(bytes: number): string {
259
+ if (bytes < 1024) return `${bytes}B`;
260
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
261
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
262
+ }