popeye-cli 1.0.0 → 1.1.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.
Files changed (171) hide show
  1. package/README.md +521 -125
  2. package/dist/adapters/claude.d.ts +16 -4
  3. package/dist/adapters/claude.d.ts.map +1 -1
  4. package/dist/adapters/claude.js +679 -33
  5. package/dist/adapters/claude.js.map +1 -1
  6. package/dist/adapters/gemini.d.ts +55 -0
  7. package/dist/adapters/gemini.d.ts.map +1 -0
  8. package/dist/adapters/gemini.js +318 -0
  9. package/dist/adapters/gemini.js.map +1 -0
  10. package/dist/adapters/openai.d.ts.map +1 -1
  11. package/dist/adapters/openai.js +41 -7
  12. package/dist/adapters/openai.js.map +1 -1
  13. package/dist/auth/claude.d.ts +11 -9
  14. package/dist/auth/claude.d.ts.map +1 -1
  15. package/dist/auth/claude.js +107 -71
  16. package/dist/auth/claude.js.map +1 -1
  17. package/dist/auth/gemini.d.ts +58 -0
  18. package/dist/auth/gemini.d.ts.map +1 -0
  19. package/dist/auth/gemini.js +172 -0
  20. package/dist/auth/gemini.js.map +1 -0
  21. package/dist/auth/index.d.ts +11 -7
  22. package/dist/auth/index.d.ts.map +1 -1
  23. package/dist/auth/index.js +23 -5
  24. package/dist/auth/index.js.map +1 -1
  25. package/dist/auth/keychain.d.ts +20 -7
  26. package/dist/auth/keychain.d.ts.map +1 -1
  27. package/dist/auth/keychain.js +85 -29
  28. package/dist/auth/keychain.js.map +1 -1
  29. package/dist/auth/openai.d.ts +2 -2
  30. package/dist/auth/openai.d.ts.map +1 -1
  31. package/dist/auth/openai.js +30 -32
  32. package/dist/auth/openai.js.map +1 -1
  33. package/dist/cli/index.d.ts.map +1 -1
  34. package/dist/cli/index.js +4 -7
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/cli/interactive.d.ts +2 -2
  37. package/dist/cli/interactive.d.ts.map +1 -1
  38. package/dist/cli/interactive.js +1380 -183
  39. package/dist/cli/interactive.js.map +1 -1
  40. package/dist/config/defaults.d.ts +6 -1
  41. package/dist/config/defaults.d.ts.map +1 -1
  42. package/dist/config/defaults.js +10 -2
  43. package/dist/config/defaults.js.map +1 -1
  44. package/dist/config/index.d.ts +10 -0
  45. package/dist/config/index.d.ts.map +1 -1
  46. package/dist/config/index.js +19 -0
  47. package/dist/config/index.js.map +1 -1
  48. package/dist/config/schema.d.ts +20 -0
  49. package/dist/config/schema.d.ts.map +1 -1
  50. package/dist/config/schema.js +7 -0
  51. package/dist/config/schema.js.map +1 -1
  52. package/dist/generators/python.d.ts.map +1 -1
  53. package/dist/generators/python.js +1 -0
  54. package/dist/generators/python.js.map +1 -1
  55. package/dist/generators/typescript.d.ts.map +1 -1
  56. package/dist/generators/typescript.js +1 -0
  57. package/dist/generators/typescript.js.map +1 -1
  58. package/dist/state/index.d.ts +108 -0
  59. package/dist/state/index.d.ts.map +1 -1
  60. package/dist/state/index.js +551 -4
  61. package/dist/state/index.js.map +1 -1
  62. package/dist/state/registry.d.ts +52 -0
  63. package/dist/state/registry.d.ts.map +1 -0
  64. package/dist/state/registry.js +215 -0
  65. package/dist/state/registry.js.map +1 -0
  66. package/dist/types/cli.d.ts +4 -0
  67. package/dist/types/cli.d.ts.map +1 -1
  68. package/dist/types/cli.js.map +1 -1
  69. package/dist/types/consensus.d.ts +69 -4
  70. package/dist/types/consensus.d.ts.map +1 -1
  71. package/dist/types/consensus.js +24 -3
  72. package/dist/types/consensus.js.map +1 -1
  73. package/dist/types/workflow.d.ts +55 -0
  74. package/dist/types/workflow.d.ts.map +1 -1
  75. package/dist/types/workflow.js +16 -0
  76. package/dist/types/workflow.js.map +1 -1
  77. package/dist/workflow/auto-fix.d.ts +45 -0
  78. package/dist/workflow/auto-fix.d.ts.map +1 -0
  79. package/dist/workflow/auto-fix.js +274 -0
  80. package/dist/workflow/auto-fix.js.map +1 -0
  81. package/dist/workflow/consensus.d.ts +44 -2
  82. package/dist/workflow/consensus.d.ts.map +1 -1
  83. package/dist/workflow/consensus.js +565 -17
  84. package/dist/workflow/consensus.js.map +1 -1
  85. package/dist/workflow/execution-mode.d.ts +10 -4
  86. package/dist/workflow/execution-mode.d.ts.map +1 -1
  87. package/dist/workflow/execution-mode.js +547 -58
  88. package/dist/workflow/execution-mode.js.map +1 -1
  89. package/dist/workflow/index.d.ts +14 -2
  90. package/dist/workflow/index.d.ts.map +1 -1
  91. package/dist/workflow/index.js +69 -6
  92. package/dist/workflow/index.js.map +1 -1
  93. package/dist/workflow/milestone-workflow.d.ts +34 -0
  94. package/dist/workflow/milestone-workflow.d.ts.map +1 -0
  95. package/dist/workflow/milestone-workflow.js +414 -0
  96. package/dist/workflow/milestone-workflow.js.map +1 -0
  97. package/dist/workflow/plan-mode.d.ts +14 -1
  98. package/dist/workflow/plan-mode.d.ts.map +1 -1
  99. package/dist/workflow/plan-mode.js +589 -47
  100. package/dist/workflow/plan-mode.js.map +1 -1
  101. package/dist/workflow/plan-storage.d.ts +142 -0
  102. package/dist/workflow/plan-storage.d.ts.map +1 -0
  103. package/dist/workflow/plan-storage.js +331 -0
  104. package/dist/workflow/plan-storage.js.map +1 -0
  105. package/dist/workflow/project-verification.d.ts +37 -0
  106. package/dist/workflow/project-verification.d.ts.map +1 -0
  107. package/dist/workflow/project-verification.js +381 -0
  108. package/dist/workflow/project-verification.js.map +1 -0
  109. package/dist/workflow/task-workflow.d.ts +37 -0
  110. package/dist/workflow/task-workflow.d.ts.map +1 -0
  111. package/dist/workflow/task-workflow.js +383 -0
  112. package/dist/workflow/task-workflow.js.map +1 -0
  113. package/dist/workflow/test-runner.d.ts +1 -0
  114. package/dist/workflow/test-runner.d.ts.map +1 -1
  115. package/dist/workflow/test-runner.js +9 -5
  116. package/dist/workflow/test-runner.js.map +1 -1
  117. package/dist/workflow/ui-designer.d.ts +82 -0
  118. package/dist/workflow/ui-designer.d.ts.map +1 -0
  119. package/dist/workflow/ui-designer.js +234 -0
  120. package/dist/workflow/ui-designer.js.map +1 -0
  121. package/dist/workflow/ui-setup.d.ts +58 -0
  122. package/dist/workflow/ui-setup.d.ts.map +1 -0
  123. package/dist/workflow/ui-setup.js +685 -0
  124. package/dist/workflow/ui-setup.js.map +1 -0
  125. package/dist/workflow/ui-verification.d.ts +114 -0
  126. package/dist/workflow/ui-verification.d.ts.map +1 -0
  127. package/dist/workflow/ui-verification.js +258 -0
  128. package/dist/workflow/ui-verification.js.map +1 -0
  129. package/dist/workflow/workflow-logger.d.ts +110 -0
  130. package/dist/workflow/workflow-logger.d.ts.map +1 -0
  131. package/dist/workflow/workflow-logger.js +267 -0
  132. package/dist/workflow/workflow-logger.js.map +1 -0
  133. package/package.json +2 -2
  134. package/src/adapters/claude.ts +815 -34
  135. package/src/adapters/gemini.ts +373 -0
  136. package/src/adapters/openai.ts +40 -7
  137. package/src/auth/claude.ts +120 -78
  138. package/src/auth/gemini.ts +207 -0
  139. package/src/auth/index.ts +28 -8
  140. package/src/auth/keychain.ts +95 -28
  141. package/src/auth/openai.ts +29 -36
  142. package/src/cli/index.ts +4 -7
  143. package/src/cli/interactive.ts +1641 -216
  144. package/src/config/defaults.ts +10 -2
  145. package/src/config/index.ts +21 -0
  146. package/src/config/schema.ts +7 -0
  147. package/src/generators/python.ts +1 -0
  148. package/src/generators/typescript.ts +1 -0
  149. package/src/state/index.ts +713 -4
  150. package/src/state/registry.ts +278 -0
  151. package/src/types/cli.ts +4 -0
  152. package/src/types/consensus.ts +65 -6
  153. package/src/types/workflow.ts +35 -0
  154. package/src/workflow/auto-fix.ts +340 -0
  155. package/src/workflow/consensus.ts +750 -16
  156. package/src/workflow/execution-mode.ts +673 -74
  157. package/src/workflow/index.ts +95 -6
  158. package/src/workflow/milestone-workflow.ts +576 -0
  159. package/src/workflow/plan-mode.ts +696 -50
  160. package/src/workflow/plan-storage.ts +482 -0
  161. package/src/workflow/project-verification.ts +471 -0
  162. package/src/workflow/task-workflow.ts +525 -0
  163. package/src/workflow/test-runner.ts +10 -5
  164. package/src/workflow/ui-designer.ts +337 -0
  165. package/src/workflow/ui-setup.ts +797 -0
  166. package/src/workflow/ui-verification.ts +357 -0
  167. package/src/workflow/workflow-logger.ts +353 -0
  168. package/tests/config/config.test.ts +1 -1
  169. package/tests/types/consensus.test.ts +3 -3
  170. package/tests/workflow/plan-mode.test.ts +213 -0
  171. package/tests/workflow/test-runner.test.ts +5 -3
@@ -1,36 +1,67 @@
1
1
  /**
2
2
  * Interactive mode
3
- * REPL-style interface for Popeye CLI
3
+ * Claude Code-style interface for Popeye CLI
4
4
  */
5
5
 
6
6
  import * as readline from 'node:readline';
7
- import { getAuthStatusForDisplay } from '../auth/index.js';
7
+ import { promises as fs } from 'node:fs';
8
+ import path from 'node:path';
9
+ import {
10
+ getAuthStatusForDisplay,
11
+ authenticateClaude,
12
+ authenticateOpenAI,
13
+ authenticateGemini,
14
+ isClaudeCLIInstalled,
15
+ checkClaudeCLIAuth,
16
+ checkGeminiAuth,
17
+ } from '../auth/index.js';
8
18
  import {
9
19
  runWorkflow,
10
20
  resumeWorkflow,
11
21
  getWorkflowStatus,
12
22
  getWorkflowSummary,
13
23
  } from '../workflow/index.js';
24
+ import {
25
+ analyzeProjectProgress,
26
+ verifyProjectCompletion,
27
+ } from '../state/index.js';
14
28
  import { generateProject } from '../generators/index.js';
15
- import { loadConfig } from '../config/index.js';
29
+ import {
30
+ discoverProjects,
31
+ formatProjectForDisplay,
32
+ } from '../state/registry.js';
33
+ import { loadConfig, saveConfig } from '../config/index.js';
16
34
  import type { ProjectSpec, OutputLanguage, OpenAIModel } from '../types/project.js';
35
+ import type { AIProvider, GeminiModel } from '../types/consensus.js';
17
36
  import {
18
- printHeader,
19
- printSection,
20
37
  printSuccess,
21
38
  printError,
22
39
  printWarning,
23
40
  printInfo,
24
41
  printKeyValue,
25
- printAuthStatus,
26
42
  startSpinner,
27
43
  succeedSpinner,
28
44
  failSpinner,
29
45
  stopSpinner,
30
- clearConsole,
31
46
  theme,
32
47
  } from './output.js';
33
48
 
49
+ // Note: startSpinner, succeedSpinner, failSpinner, stopSpinner are used in handleIdea
50
+
51
+ /**
52
+ * Box drawing characters for Claude Code-style UI
53
+ */
54
+ const box = {
55
+ topLeft: '╭',
56
+ topRight: '╮',
57
+ bottomLeft: '╰',
58
+ bottomRight: '╯',
59
+ horizontal: '─',
60
+ vertical: '│',
61
+ leftT: '├',
62
+ rightT: '┤',
63
+ };
64
+
34
65
  /**
35
66
  * Interactive session state
36
67
  */
@@ -38,159 +69,615 @@ interface SessionState {
38
69
  projectDir: string | null;
39
70
  language: OutputLanguage;
40
71
  model: OpenAIModel;
41
- authenticated: boolean;
72
+ geminiModel: GeminiModel;
73
+ claudeAuth: boolean;
74
+ openaiAuth: boolean;
75
+ geminiAuth: boolean;
76
+ reviewer: AIProvider;
77
+ arbitrator: AIProvider;
78
+ enableArbitration: boolean;
42
79
  }
43
80
 
44
81
  /**
45
- * Command handler type
82
+ * Get terminal width
46
83
  */
47
- type CommandHandler = (args: string[], state: SessionState) => Promise<void>;
84
+ function getTerminalWidth(): number {
85
+ return process.stdout.columns || 80;
86
+ }
48
87
 
49
88
  /**
50
- * Available slash commands
89
+ * Draw the header box
51
90
  */
52
- const commands: Record<string, { description: string; handler: CommandHandler }> = {
53
- '/help': {
54
- description: 'Show available commands',
55
- handler: handleHelp,
56
- },
57
- '/status': {
58
- description: 'Show current project status',
59
- handler: handleStatus,
60
- },
61
- '/auth': {
62
- description: 'Check authentication status',
63
- handler: handleAuth,
64
- },
65
- '/config': {
66
- description: 'Show current configuration',
67
- handler: handleConfig,
68
- },
69
- '/language': {
70
- description: 'Set output language (python/typescript)',
71
- handler: handleLanguage,
72
- },
73
- '/model': {
74
- description: 'Set OpenAI model',
75
- handler: handleModel,
76
- },
77
- '/project': {
78
- description: 'Set project directory',
79
- handler: handleProject,
80
- },
81
- '/resume': {
82
- description: 'Resume current project workflow',
83
- handler: handleResume,
84
- },
85
- '/clear': {
86
- description: 'Clear the screen',
87
- handler: handleClear,
88
- },
89
- '/exit': {
90
- description: 'Exit interactive mode',
91
- handler: handleExit,
92
- },
93
- '/quit': {
94
- description: 'Exit interactive mode',
95
- handler: handleExit,
96
- },
97
- };
91
+ function drawHeader(): void {
92
+ const width = getTerminalWidth();
93
+ const title = ' Popeye CLI ';
94
+ const subtitle = ' Autonomous Code Generation with AI Consensus ';
95
+
96
+ // Top border
97
+ console.log(theme.dim(box.topLeft + box.horizontal.repeat(width - 2) + box.topRight));
98
+
99
+ // Title line
100
+ const titlePadding = Math.floor((width - title.length - 2) / 2);
101
+ console.log(
102
+ theme.dim(box.vertical) +
103
+ ' '.repeat(titlePadding) +
104
+ theme.primary.bold(title) +
105
+ ' '.repeat(width - titlePadding - title.length - 2) +
106
+ theme.dim(box.vertical)
107
+ );
108
+
109
+ // Subtitle line
110
+ const subPadding = Math.floor((width - subtitle.length - 2) / 2);
111
+ console.log(
112
+ theme.dim(box.vertical) +
113
+ ' '.repeat(subPadding) +
114
+ theme.secondary(subtitle) +
115
+ ' '.repeat(width - subPadding - subtitle.length - 2) +
116
+ theme.dim(box.vertical)
117
+ );
118
+
119
+ // Bottom border
120
+ console.log(theme.dim(box.bottomLeft + box.horizontal.repeat(width - 2) + box.bottomRight));
121
+ }
98
122
 
99
123
  /**
100
- * Start interactive mode
124
+ * Draw hints line and top of input box
101
125
  */
102
- export async function startInteractiveMode(): Promise<void> {
103
- clearConsole();
104
- printHeader('Popeye CLI - Interactive Mode');
126
+ function drawInputBoxTop(state: SessionState): void {
127
+ const width = Math.min(getTerminalWidth(), 100);
105
128
 
106
- printInfo('Type your project idea or use /help for commands');
129
+ // Hints line (above the box)
130
+ const hints = [
131
+ theme.dim('/lang ') + theme.primary('py') + theme.dim('|') + theme.primary('ts'),
132
+ theme.dim('/config'),
133
+ theme.dim('/help'),
134
+ theme.dim('/exit'),
135
+ ];
136
+ console.log(' ' + hints.join(' '));
137
+
138
+ // Status items for the top line
139
+ const langStatus = state.language;
140
+ const reviewerStatus = state.reviewer === 'openai' ? 'O' : 'G';
141
+ const arbitratorStatus = state.enableArbitration ? (state.arbitrator === 'openai' ? 'O' : 'G') : '-';
142
+ const allAuth = state.claudeAuth && state.openaiAuth && (state.enableArbitration ? state.geminiAuth : true);
143
+ const authIcon = allAuth ? '●' : '○';
144
+ const authColor = allAuth ? theme.success : theme.warning;
145
+
146
+ // Build status text
147
+ const statusParts = [
148
+ theme.primary(langStatus),
149
+ theme.dim('R:') + theme.secondary(reviewerStatus),
150
+ theme.dim('A:') + theme.secondary(arbitratorStatus),
151
+ authColor(authIcon),
152
+ ];
153
+ const statusText = statusParts.join(theme.dim(' │ '));
154
+
155
+ // Calculate visible length (without ANSI codes)
156
+ // eslint-disable-next-line no-control-regex
157
+ const stripAnsi = (str: string) => str.replace(/\x1b\[[0-9;]*m/g, '');
158
+ const statusLen = stripAnsi(statusText).length;
159
+
160
+ // Top line: ╭─ status ─────────────────────────────────────────╮
161
+ const paddingLen = Math.max(0, width - statusLen - 6);
162
+ console.log(
163
+ theme.dim(box.topLeft + box.horizontal + ' ') +
164
+ statusText +
165
+ theme.dim(' ' + box.horizontal.repeat(paddingLen) + box.topRight)
166
+ );
167
+ }
168
+
169
+ /**
170
+ * Draw bottom of input box after user presses enter
171
+ */
172
+ function drawInputBoxBottom(): void {
173
+ const width = Math.min(getTerminalWidth(), 100);
174
+ console.log(theme.dim(box.bottomLeft + box.horizontal.repeat(width - 2) + box.bottomRight));
175
+ }
176
+
177
+ /**
178
+ * Clear screen and redraw UI
179
+ */
180
+ function redrawUI(_state: SessionState): void {
181
+ console.clear();
182
+ drawHeader();
107
183
  console.log();
184
+ }
108
185
 
109
- // Initialize state
110
- const config = await loadConfig();
111
- const state: SessionState = {
112
- projectDir: process.cwd(),
113
- language: config.project.default_language,
114
- model: config.apis.openai.model,
115
- authenticated: false,
116
- };
186
+ /**
187
+ * Prompt for input with styled prompt (inside box)
188
+ */
189
+ function getPrompt(): string {
190
+ return theme.dim(box.vertical + ' ') + theme.primary('popeye') + theme.dim(' > ');
191
+ }
117
192
 
118
- // Check authentication
119
- const authStatus = await getAuthStatusForDisplay();
120
- state.authenticated = authStatus.claude.authenticated && authStatus.openai.authenticated;
193
+ /**
194
+ * Prompt user to select an option
195
+ * Uses terminal: false to prevent echo issues when nested with main readline
196
+ */
197
+ async function promptSelection(
198
+ question: string,
199
+ options: { label: string; value: string }[],
200
+ defaultValue: string
201
+ ): Promise<string> {
202
+ return new Promise((resolve) => {
203
+ const rl = readline.createInterface({
204
+ input: process.stdin,
205
+ output: process.stdout,
206
+ terminal: false, // Prevent terminal mode to avoid echo issues
207
+ });
121
208
 
122
- if (!state.authenticated) {
123
- printWarning('Not fully authenticated. Run /auth to check status.');
124
209
  console.log();
125
- }
210
+ console.log(theme.primary(` ${question}`));
211
+ options.forEach((opt, i) => {
212
+ const isDefault = opt.value === defaultValue;
213
+ console.log(` ${theme.dim(`${i + 1}.`)} ${opt.label}${isDefault ? theme.dim(' (default)') : ''}`);
214
+ });
215
+ console.log();
126
216
 
127
- // Create readline interface
128
- const rl = readline.createInterface({
129
- input: process.stdin,
130
- output: process.stdout,
131
- prompt: theme.primary('popeye> '),
217
+ // Print prompt manually since terminal: false disables it
218
+ process.stdout.write(` Enter choice [1-${options.length}] or press Enter for default: `);
219
+
220
+ rl.once('line', (answer) => {
221
+ rl.close();
222
+ const trimmed = answer.trim();
223
+ if (!trimmed) {
224
+ resolve(defaultValue);
225
+ return;
226
+ }
227
+ const num = parseInt(trimmed, 10);
228
+ if (num >= 1 && num <= options.length) {
229
+ resolve(options[num - 1].value);
230
+ } else {
231
+ resolve(defaultValue);
232
+ }
233
+ });
132
234
  });
235
+ }
133
236
 
134
- rl.prompt();
237
+ /**
238
+ * Prompt yes/no question
239
+ * Uses terminal: false to prevent echo issues when nested with main readline
240
+ */
241
+ async function promptYesNo(question: string, defaultYes: boolean = true): Promise<boolean> {
242
+ return new Promise((resolve) => {
243
+ const rl = readline.createInterface({
244
+ input: process.stdin,
245
+ output: process.stdout,
246
+ terminal: false, // Prevent terminal mode to avoid echo issues
247
+ });
135
248
 
136
- rl.on('line', async (line) => {
137
- const input = line.trim();
249
+ const hint = defaultYes ? '[Y/n]' : '[y/N]';
250
+ process.stdout.write(` ${question} ${theme.dim(hint)} `);
138
251
 
139
- if (!input) {
140
- rl.prompt();
141
- return;
252
+ rl.once('line', (answer) => {
253
+ rl.close();
254
+ const trimmed = answer.trim().toLowerCase();
255
+ if (!trimmed) {
256
+ resolve(defaultYes);
257
+ } else {
258
+ resolve(trimmed === 'y' || trimmed === 'yes');
259
+ }
260
+ });
261
+ });
262
+ }
263
+
264
+ /**
265
+ * Check if reviewer/arbitrator has been configured (saved to config)
266
+ */
267
+ async function isConsensusConfigured(): Promise<boolean> {
268
+ const config = await loadConfig();
269
+ // Consider configured if enable_arbitration is true or if arbitrator is explicitly set to a provider
270
+ // (Default is arbitrator='off' and enable_arbitration=false, so any change indicates user configured it)
271
+ return config.consensus.enable_arbitration || config.consensus.arbitrator !== 'off';
272
+ }
273
+
274
+ /**
275
+ * Save reviewer/arbitrator settings to config file
276
+ */
277
+ async function saveConsensusConfig(state: SessionState): Promise<void> {
278
+ try {
279
+ // Load existing config and merge with new consensus settings
280
+ const existingConfig = await loadConfig();
281
+ const updatedConsensus = {
282
+ ...existingConfig.consensus,
283
+ reviewer: state.reviewer,
284
+ arbitrator: state.enableArbitration ? state.arbitrator : 'off' as const,
285
+ enable_arbitration: state.enableArbitration,
286
+ };
287
+ await saveConfig({
288
+ consensus: updatedConsensus,
289
+ }, true); // Save to global config
290
+ } catch (err) {
291
+ printWarning(`Could not save config: ${err instanceof Error ? err.message : 'Unknown error'}`);
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Check and perform authentication
297
+ */
298
+ async function ensureAuthentication(state: SessionState): Promise<boolean> {
299
+ const status = await getAuthStatusForDisplay();
300
+ state.claudeAuth = status.claude.authenticated;
301
+ state.openaiAuth = status.openai.authenticated;
302
+ state.geminiAuth = status.gemini?.authenticated || false;
303
+
304
+ console.log();
305
+ printInfo('Checking authentication...');
306
+ console.log();
307
+
308
+ // Authenticate Claude if needed
309
+ if (!state.claudeAuth) {
310
+ console.log(theme.dim(box.vertical) + ' ' + theme.primary('Claude Code CLI') + theme.dim(' - Required for code generation'));
311
+ console.log(theme.dim(box.vertical));
312
+
313
+ try {
314
+ const success = await authenticateClaude();
315
+ if (success) {
316
+ printSuccess('Claude Code CLI ready');
317
+ state.claudeAuth = true;
318
+ } else {
319
+ printWarning('Claude Code CLI not authenticated - run "claude login" to authenticate');
320
+ }
321
+ } catch (err) {
322
+ printError(err instanceof Error ? err.message : 'Authentication failed');
142
323
  }
324
+ console.log();
325
+ } else {
326
+ printSuccess('Claude Code CLI ready');
327
+ }
328
+
329
+ // Authenticate OpenAI if needed
330
+ if (!state.openaiAuth) {
331
+ console.log(theme.dim(box.vertical) + ' ' + theme.primary('OpenAI API') + theme.dim(' - Required for consensus review'));
332
+ console.log(theme.dim(box.vertical));
143
333
 
144
334
  try {
145
- if (input.startsWith('/')) {
146
- // Handle command
147
- const [command, ...args] = input.split(/\s+/);
148
- const cmd = commands[command.toLowerCase()];
335
+ const success = await authenticateOpenAI();
336
+ if (success) {
337
+ printSuccess('OpenAI API ready');
338
+ state.openaiAuth = true;
339
+ } else {
340
+ printWarning('OpenAI API not authenticated');
341
+ }
342
+ } catch (err) {
343
+ printError(err instanceof Error ? err.message : 'Authentication failed');
344
+ }
345
+ console.log();
346
+ } else {
347
+ printSuccess('OpenAI API ready');
348
+ }
349
+
350
+ // Check if reviewer/arbitrator is already configured
351
+ const alreadyConfigured = await isConsensusConfigured();
352
+
353
+ // Only ask about reviewer/arbitrator if not already configured
354
+ if (state.claudeAuth && state.openaiAuth && !alreadyConfigured) {
355
+ console.log();
356
+ console.log(theme.primary.bold(' AI Configuration'));
357
+ console.log(theme.dim(' Claude generates code. Choose who reviews and arbitrates:'));
358
+
359
+ // Ask who should review plans
360
+ state.reviewer = await promptSelection(
361
+ 'Who should review Claude\'s plans?',
362
+ [
363
+ { label: theme.secondary('OpenAI') + theme.dim(' - GPT-4o reviews plans'), value: 'openai' },
364
+ { label: theme.secondary('Gemini') + theme.dim(' - Gemini 2.0 reviews plans'), value: 'gemini' },
365
+ ],
366
+ 'openai'
367
+ ) as AIProvider;
368
+
369
+ // Ask about arbitration
370
+ console.log();
371
+ state.enableArbitration = await promptYesNo(
372
+ theme.primary('Enable arbitration when consensus is stuck?'),
373
+ true
374
+ );
375
+
376
+ if (state.enableArbitration) {
377
+ // Auto-select the other provider as arbitrator
378
+ const defaultArbitrator = state.reviewer === 'openai' ? 'gemini' : 'openai';
379
+
380
+ state.arbitrator = await promptSelection(
381
+ 'Who should arbitrate when stuck?',
382
+ [
383
+ { label: theme.secondary('Gemini') + theme.dim(' - Google Gemini breaks deadlocks'), value: 'gemini' },
384
+ { label: theme.secondary('OpenAI') + theme.dim(' - OpenAI breaks deadlocks'), value: 'openai' },
385
+ ],
386
+ defaultArbitrator
387
+ ) as AIProvider;
388
+
389
+ // Authenticate Gemini if needed for reviewer or arbitrator
390
+ const needsGemini = state.reviewer === 'gemini' || state.arbitrator === 'gemini';
391
+ if (needsGemini && !state.geminiAuth) {
392
+ console.log();
393
+ console.log(theme.dim(box.vertical) + ' ' + theme.primary('Gemini API') + theme.dim(' - Required for ' + (state.reviewer === 'gemini' ? 'review' : 'arbitration')));
394
+ console.log(theme.dim(box.vertical));
395
+
396
+ try {
397
+ const success = await authenticateGemini();
398
+ if (success) {
399
+ printSuccess('Gemini API ready');
400
+ state.geminiAuth = true;
401
+ } else {
402
+ printWarning('Gemini API not authenticated - arbitration disabled');
403
+ state.enableArbitration = false;
404
+ }
405
+ } catch (err) {
406
+ printError(err instanceof Error ? err.message : 'Authentication failed');
407
+ state.enableArbitration = false;
408
+ }
409
+ }
410
+ }
149
411
 
150
- if (cmd) {
151
- await cmd.handler(args, state);
412
+ // Also check if reviewer is gemini and we need to auth
413
+ if (state.reviewer === 'gemini' && !state.geminiAuth) {
414
+ console.log();
415
+ console.log(theme.dim(box.vertical) + ' ' + theme.primary('Gemini API') + theme.dim(' - Required for review'));
416
+ console.log(theme.dim(box.vertical));
417
+
418
+ try {
419
+ const success = await authenticateGemini();
420
+ if (success) {
421
+ printSuccess('Gemini API ready');
422
+ state.geminiAuth = true;
152
423
  } else {
153
- printError(`Unknown command: ${command}. Use /help for available commands.`);
424
+ printWarning('Gemini API not authenticated - falling back to OpenAI');
425
+ state.reviewer = 'openai';
154
426
  }
155
- } else {
156
- // Treat as project idea
157
- await handleIdea(input, state);
427
+ } catch (err) {
428
+ printError(err instanceof Error ? err.message : 'Authentication failed');
429
+ state.reviewer = 'openai';
158
430
  }
159
- } catch (error) {
160
- printError(error instanceof Error ? error.message : 'Unknown error');
161
431
  }
162
432
 
163
- console.log();
164
- rl.prompt();
165
- });
433
+ // Save the configuration to persist between sessions
434
+ await saveConsensusConfig(state);
166
435
 
167
- rl.on('close', () => {
436
+ // Show summary
168
437
  console.log();
169
- printInfo('Goodbye!');
170
- process.exit(0);
171
- });
438
+ console.log(theme.secondary(' Configuration saved. Use /config to change later.'));
439
+ console.log(` ${theme.dim('Reviewer:')} ${theme.primary(state.reviewer === 'openai' ? 'OpenAI (GPT-4o)' : 'Gemini')}`);
440
+ console.log(` ${theme.dim('Arbitrator:')} ${state.enableArbitration ? theme.primary(state.arbitrator === 'openai' ? 'OpenAI' : 'Gemini') : theme.dim('Disabled')}`);
441
+ console.log();
442
+ } else if (state.claudeAuth && state.openaiAuth && alreadyConfigured) {
443
+ // Show loaded configuration
444
+ console.log();
445
+ console.log(theme.secondary(' Using saved configuration (use /config to change):'));
446
+ console.log(` ${theme.dim('Reviewer:')} ${theme.primary(state.reviewer === 'openai' ? 'OpenAI (GPT-4o)' : 'Gemini')}`);
447
+ console.log(` ${theme.dim('Arbitrator:')} ${state.enableArbitration ? theme.primary(state.arbitrator === 'openai' ? 'OpenAI' : 'Gemini') : theme.dim('Disabled')}`);
448
+ console.log();
449
+
450
+ // Authenticate Gemini if needed based on saved config
451
+ const needsGemini = state.reviewer === 'gemini' || (state.enableArbitration && state.arbitrator === 'gemini');
452
+ if (needsGemini && !state.geminiAuth) {
453
+ console.log(theme.dim(box.vertical) + ' ' + theme.primary('Gemini API') + theme.dim(' - Required for ' + (state.reviewer === 'gemini' ? 'review' : 'arbitration')));
454
+ console.log(theme.dim(box.vertical));
455
+
456
+ try {
457
+ const success = await authenticateGemini();
458
+ if (success) {
459
+ printSuccess('Gemini API ready');
460
+ state.geminiAuth = true;
461
+ } else {
462
+ printWarning('Gemini API not authenticated');
463
+ if (state.reviewer === 'gemini') {
464
+ printWarning('Falling back to OpenAI as reviewer');
465
+ state.reviewer = 'openai';
466
+ }
467
+ if (state.enableArbitration && state.arbitrator === 'gemini') {
468
+ printWarning('Disabling arbitration');
469
+ state.enableArbitration = false;
470
+ }
471
+ }
472
+ } catch (err) {
473
+ printError(err instanceof Error ? err.message : 'Gemini authentication failed');
474
+ }
475
+ console.log();
476
+ }
477
+ }
478
+
479
+ return state.claudeAuth && state.openaiAuth;
480
+ }
481
+
482
+ /**
483
+ * Display help
484
+ */
485
+ function showHelp(): void {
486
+ console.log();
487
+ console.log(theme.primary.bold(' Commands:'));
488
+ console.log();
489
+
490
+ const commands = [
491
+ ['/help', 'Show this help message'],
492
+ ['/info', 'Show system info (Claude CLI status, etc.)'],
493
+ ['/status', 'Show current project status'],
494
+ ['/auth', 'Re-authenticate services'],
495
+ ['/config', 'Show/change configuration'],
496
+ ['/config reviewer', 'Set reviewer (openai/gemini)'],
497
+ ['/config arbitrator', 'Set arbitrator (openai/gemini/off)'],
498
+ ['/lang <lang>', 'Set language (python/typescript)'],
499
+ ['/new <idea>', 'Force start a new project (skips existing check)'],
500
+ ['/resume', 'Resume interrupted project'],
501
+ ['/clear', 'Clear screen'],
502
+ ['/exit', 'Exit Popeye'],
503
+ ];
504
+
505
+ for (const [cmd, desc] of commands) {
506
+ console.log(` ${theme.primary(cmd.padEnd(20))} ${theme.dim(desc)}`);
507
+ }
508
+
509
+ console.log();
510
+ console.log(theme.secondary(' Type your project idea to get started!'));
511
+ console.log(theme.secondary(' Example: "A REST API for managing todo items"'));
512
+ console.log();
172
513
  }
173
514
 
174
515
  /**
175
- * Handle /help command
516
+ * Handle /info command - show system info
176
517
  */
177
- async function handleHelp(_args: string[], _state: SessionState): Promise<void> {
178
- printSection('Available Commands');
518
+ async function handleInfo(): Promise<void> {
519
+ console.log();
520
+ console.log(theme.primary.bold(' System Info:'));
521
+ console.log();
522
+
523
+ // Check Claude CLI
524
+ const claudeInstalled = await isClaudeCLIInstalled();
525
+ const claudeStatus = await checkClaudeCLIAuth();
526
+
527
+ console.log(theme.secondary(' Claude Code:'));
528
+ console.log(` ${theme.dim('Installed:')} ${claudeInstalled ? theme.success('Yes') : theme.error('No')}`);
529
+
530
+ if (claudeInstalled) {
531
+ console.log(` ${theme.dim('Authenticated:')} ${claudeStatus.authenticated ? theme.success('Yes') : theme.warning('No')}`);
532
+ console.log(` ${theme.dim('Model:')} ${theme.primary('Uses your Claude Code settings')}`);
533
+ console.log(` ${theme.dim('MCPs:')} ${theme.primary('Uses your configured MCP servers')}`);
534
+
535
+ if (!claudeStatus.authenticated) {
536
+ console.log();
537
+ console.log(` ${theme.warning('Run:')} ${theme.primary('claude login')} ${theme.warning('to authenticate')}`);
538
+ }
539
+ } else {
540
+ console.log();
541
+ console.log(` ${theme.warning('Install:')} ${theme.primary('npm install -g @anthropic-ai/claude-code')}`);
542
+ }
543
+
544
+ console.log();
545
+ console.log(theme.secondary(' OpenAI:'));
546
+ const authStatus = await getAuthStatusForDisplay();
547
+ console.log(` ${theme.dim('Authenticated:')} ${authStatus.openai.authenticated ? theme.success('Yes') : theme.warning('No')}`);
548
+ if (authStatus.openai.authenticated && authStatus.openai.keyLastFour) {
549
+ console.log(` ${theme.dim('API Key:')} ${theme.dim(authStatus.openai.keyLastFour)}`);
550
+ }
179
551
 
180
- for (const [cmd, info] of Object.entries(commands)) {
181
- console.log(` ${theme.primary(cmd.padEnd(15))} ${info.description}`);
552
+ console.log();
553
+ console.log(theme.secondary(' Gemini:'));
554
+ const geminiStatus = await checkGeminiAuth();
555
+ console.log(` ${theme.dim('Authenticated:')} ${geminiStatus.authenticated ? theme.success('Yes') : theme.dim('No')}`);
556
+ if (geminiStatus.authenticated && geminiStatus.keyLastFour) {
557
+ console.log(` ${theme.dim('API Key:')} ${theme.dim(geminiStatus.keyLastFour)}`);
182
558
  }
183
559
 
184
560
  console.log();
185
- printInfo('Or type a project idea to start creating');
561
+ console.log(theme.secondary(' Environment:'));
562
+ console.log(` ${theme.dim('Node.js:')} ${process.version}`);
563
+ console.log(` ${theme.dim('Platform:')} ${process.platform}`);
564
+ console.log(` ${theme.dim('Working Dir:')} ${process.cwd()}`);
565
+ console.log();
566
+
567
+ console.log(theme.dim(' Tip: Claude Code model and MCP settings are configured in your'));
568
+ console.log(theme.dim(' Claude Code CLI. Run "claude config" to see/change them.'));
569
+ console.log();
570
+ }
571
+
572
+ /**
573
+ * Handle a command or idea
574
+ */
575
+ async function handleInput(input: string, state: SessionState): Promise<boolean> {
576
+ const trimmed = input.trim();
577
+
578
+ if (!trimmed) return true;
579
+
580
+ // Check for common words that should be commands (without /)
581
+ const lowerTrimmed = trimmed.toLowerCase();
582
+ if (['help', 'exit', 'quit', 'info', 'status', 'config'].includes(lowerTrimmed)) {
583
+ printWarning(`Did you mean /${lowerTrimmed}? Use / prefix for commands.`);
584
+ return true;
585
+ }
586
+
587
+ // Handle commands
588
+ if (trimmed.startsWith('/')) {
589
+ const [cmd, ...args] = trimmed.split(/\s+/);
590
+ const command = cmd.toLowerCase();
591
+
592
+ switch (command) {
593
+ case '/help':
594
+ case '/h':
595
+ case '/?':
596
+ showHelp();
597
+ break;
598
+
599
+ case '/info':
600
+ case '/check':
601
+ await handleInfo();
602
+ break;
603
+
604
+ case '/exit':
605
+ case '/quit':
606
+ case '/q':
607
+ console.log();
608
+ printInfo('Goodbye!');
609
+ return false;
610
+
611
+ case '/clear':
612
+ case '/cls':
613
+ redrawUI(state);
614
+ break;
615
+
616
+ case '/status':
617
+ await handleStatus(state);
618
+ break;
619
+
620
+ case '/auth':
621
+ await ensureAuthentication(state);
622
+ break;
623
+
624
+ case '/config':
625
+ await handleConfig(state, args);
626
+ break;
627
+
628
+ case '/language':
629
+ case '/lang':
630
+ case '/l':
631
+ handleLanguage(args, state);
632
+ break;
633
+
634
+ case '/model':
635
+ case '/m':
636
+ handleModel(args, state);
637
+ break;
638
+
639
+ case '/resume':
640
+ await handleResume(state, args);
641
+ break;
642
+
643
+ case '/new':
644
+ // Force start a new project even if existing projects found
645
+ if (args.length === 0) {
646
+ printError('Usage: /new <project idea>');
647
+ printInfo('Example: /new todo app with user authentication');
648
+ } else {
649
+ await handleNewProject(args.join(' '), state);
650
+ }
651
+ break;
652
+
653
+ default:
654
+ printError(`Unknown command: ${cmd}`);
655
+ printInfo('Type /help for available commands');
656
+ }
657
+
658
+ return true;
659
+ }
660
+
661
+ // Warn if input is too short (likely accidental)
662
+ if (trimmed.length < 10) {
663
+ printWarning(`Input "${trimmed}" is very short. Did you mean to type a command?`);
664
+ printInfo('Type /help for commands, or enter a longer project description.');
665
+ return true;
666
+ }
667
+
668
+ // Handle as project idea
669
+ await handleIdea(trimmed, state);
670
+ return true;
186
671
  }
187
672
 
188
673
  /**
189
674
  * Handle /status command
190
675
  */
191
- async function handleStatus(_args: string[], state: SessionState): Promise<void> {
676
+ async function handleStatus(state: SessionState): Promise<void> {
677
+ console.log();
678
+
192
679
  if (!state.projectDir) {
193
- printInfo('No project directory set. Use /project <dir> to set one.');
680
+ printInfo('No active project');
194
681
  return;
195
682
  }
196
683
 
@@ -199,8 +686,6 @@ async function handleStatus(_args: string[], state: SessionState): Promise<void>
199
686
  if (!status.exists) {
200
687
  printInfo('No project found in current directory');
201
688
  printKeyValue('Directory', state.projectDir);
202
- printKeyValue('Language', state.language);
203
- printKeyValue('Model', state.model);
204
689
  return;
205
690
  }
206
691
 
@@ -209,42 +694,118 @@ async function handleStatus(_args: string[], state: SessionState): Promise<void>
209
694
  }
210
695
 
211
696
  /**
212
- * Handle /auth command
697
+ * Handle /config command
213
698
  */
214
- async function handleAuth(_args: string[], state: SessionState): Promise<void> {
215
- const status = await getAuthStatusForDisplay();
216
- printAuthStatus(status);
699
+ async function handleConfig(state: SessionState, args: string[] = []): Promise<void> {
700
+ const config = await loadConfig();
217
701
 
218
- state.authenticated = status.claude.authenticated && status.openai.authenticated;
702
+ // Handle config subcommands
703
+ if (args.length > 0) {
704
+ const subcommand = args[0].toLowerCase();
219
705
 
220
- if (!state.authenticated) {
221
- console.log();
222
- printInfo('Run "popeye-cli auth login" to authenticate');
223
- }
224
- }
706
+ switch (subcommand) {
707
+ case 'reviewer':
708
+ if (args.length > 1) {
709
+ const newReviewer = args[1].toLowerCase();
710
+ if (newReviewer === 'openai' || newReviewer === 'gemini') {
711
+ if (newReviewer === 'gemini' && !state.geminiAuth) {
712
+ printWarning('Gemini API not authenticated. Run /auth first.');
713
+ return;
714
+ }
715
+ state.reviewer = newReviewer as AIProvider;
716
+ // Save to config
717
+ await saveConsensusConfig(state);
718
+ printSuccess(`Reviewer set to ${newReviewer}`);
719
+ } else {
720
+ printError('Invalid reviewer. Use: openai or gemini');
721
+ }
722
+ } else {
723
+ printKeyValue('Reviewer', state.reviewer);
724
+ printInfo('Use: /config reviewer <openai|gemini>');
725
+ }
726
+ return;
225
727
 
226
- /**
227
- * Handle /config command
228
- */
229
- async function handleConfig(_args: string[], state: SessionState): Promise<void> {
230
- const config = await loadConfig();
728
+ case 'arbitrator':
729
+ if (args.length > 1) {
730
+ const newArbitrator = args[1].toLowerCase();
731
+ if (newArbitrator === 'openai' || newArbitrator === 'gemini') {
732
+ if (newArbitrator === 'gemini' && !state.geminiAuth) {
733
+ printWarning('Gemini API not authenticated. Run /auth first.');
734
+ return;
735
+ }
736
+ state.arbitrator = newArbitrator as AIProvider;
737
+ state.enableArbitration = true;
738
+ // Save to config
739
+ await saveConsensusConfig(state);
740
+ printSuccess(`Arbitrator set to ${newArbitrator}`);
741
+ } else if (newArbitrator === 'off' || newArbitrator === 'none') {
742
+ state.enableArbitration = false;
743
+ // Save to config
744
+ await saveConsensusConfig(state);
745
+ printSuccess('Arbitration disabled');
746
+ } else {
747
+ printError('Invalid arbitrator. Use: openai, gemini, or off');
748
+ }
749
+ } else {
750
+ printKeyValue('Arbitrator', state.enableArbitration ? state.arbitrator : 'disabled');
751
+ printInfo('Use: /config arbitrator <openai|gemini|off>');
752
+ }
753
+ return;
754
+
755
+ case 'language':
756
+ case 'lang':
757
+ if (args.length > 1) {
758
+ const lang = args[1].toLowerCase() as OutputLanguage;
759
+ if (['python', 'typescript'].includes(lang)) {
760
+ state.language = lang;
761
+ printSuccess(`Language set to ${lang}`);
762
+ } else {
763
+ printError('Invalid language. Use: python or typescript');
764
+ }
765
+ } else {
766
+ printKeyValue('Language', state.language);
767
+ }
768
+ return;
231
769
 
232
- printSection('Current Session');
233
- printKeyValue('Project Dir', state.projectDir || 'Not set');
234
- printKeyValue('Language', state.language);
235
- printKeyValue('Model', state.model);
236
- printKeyValue('Authenticated', state.authenticated ? 'Yes' : 'No');
770
+ default:
771
+ printError(`Unknown config option: ${subcommand}`);
772
+ printInfo('Options: reviewer, arbitrator, language');
773
+ return;
774
+ }
775
+ }
237
776
 
238
- printSection('Configuration');
239
- printKeyValue('Threshold', `${config.consensus.threshold}%`);
240
- printKeyValue('Max Iterations', config.consensus.max_disagreements.toString());
777
+ // Show full config
778
+ console.log();
779
+ console.log(theme.primary.bold(' Session:'));
780
+ console.log(` ${theme.dim('Directory:')} ${state.projectDir || 'Not set'}`);
781
+ console.log(` ${theme.dim('Language:')} ${theme.primary(state.language)}`);
782
+ console.log();
783
+ console.log(theme.primary.bold(' Authentication:'));
784
+ console.log(` ${theme.dim('Claude:')} ${state.claudeAuth ? theme.success('● Ready') : theme.error('○ Not authenticated')}`);
785
+ console.log(` ${theme.dim('OpenAI:')} ${state.openaiAuth ? theme.success('● Ready') : theme.error('○ Not authenticated')}`);
786
+ console.log(` ${theme.dim('Gemini:')} ${state.geminiAuth ? theme.success('● Ready') : theme.dim('○ Not configured')}`);
787
+ console.log();
788
+ console.log(theme.primary.bold(' AI Configuration:'));
789
+ console.log(` ${theme.dim('Reviewer:')} ${theme.primary(state.reviewer === 'openai' ? 'OpenAI (GPT-4o)' : 'Gemini')}`);
790
+ console.log(` ${theme.dim('Arbitrator:')} ${state.enableArbitration ? theme.primary(state.arbitrator === 'openai' ? 'OpenAI' : 'Gemini') : theme.dim('Disabled')}`);
791
+ console.log();
792
+ console.log(theme.primary.bold(' Consensus:'));
793
+ console.log(` ${theme.dim('Threshold:')} ${config.consensus.threshold}%`);
794
+ console.log(` ${theme.dim('Max Iters:')} ${config.consensus.max_disagreements}`);
795
+ console.log();
796
+ console.log(theme.secondary(' Change settings:'));
797
+ console.log(theme.dim(' /config reviewer <openai|gemini>'));
798
+ console.log(theme.dim(' /config arbitrator <openai|gemini|off>'));
799
+ console.log(theme.dim(' /config language <python|typescript>'));
800
+ console.log();
241
801
  }
242
802
 
243
803
  /**
244
804
  * Handle /language command
245
805
  */
246
- async function handleLanguage(args: string[], state: SessionState): Promise<void> {
806
+ function handleLanguage(args: string[], state: SessionState): void {
247
807
  if (args.length === 0) {
808
+ console.log();
248
809
  printKeyValue('Current language', state.language);
249
810
  printInfo('Use /language <python|typescript> to change');
250
811
  return;
@@ -257,18 +818,20 @@ async function handleLanguage(args: string[], state: SessionState): Promise<void
257
818
  }
258
819
 
259
820
  state.language = lang;
260
- printSuccess(`Language set to: ${lang}`);
821
+ console.log();
822
+ printSuccess(`Language set to ${lang}`);
261
823
  }
262
824
 
263
825
  /**
264
826
  * Handle /model command
265
827
  */
266
- async function handleModel(args: string[], state: SessionState): Promise<void> {
828
+ function handleModel(args: string[], state: SessionState): void {
267
829
  const validModels = ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'o1-preview', 'o1-mini'];
268
830
 
269
831
  if (args.length === 0) {
832
+ console.log();
270
833
  printKeyValue('Current model', state.model);
271
- printInfo(`Use /model <${validModels.join('|')}> to change`);
834
+ printInfo(`Available: ${validModels.join(', ')}`);
272
835
  return;
273
836
  }
274
837
 
@@ -279,116 +842,793 @@ async function handleModel(args: string[], state: SessionState): Promise<void> {
279
842
  }
280
843
 
281
844
  state.model = model;
282
- printSuccess(`Model set to: ${model}`);
845
+ console.log();
846
+ printSuccess(`Model set to ${model}`);
283
847
  }
284
848
 
285
849
  /**
286
- * Handle /project command
850
+ * Prompt for additional context
851
+ * Uses terminal: false to prevent echo issues when nested with main readline
287
852
  */
288
- async function handleProject(args: string[], state: SessionState): Promise<void> {
289
- const path = await import('node:path');
853
+ async function promptForContext(prompt: string): Promise<string> {
854
+ return new Promise((resolve) => {
855
+ const rl = readline.createInterface({
856
+ input: process.stdin,
857
+ output: process.stdout,
858
+ terminal: false, // Prevent terminal mode to avoid echo issues
859
+ });
290
860
 
291
- if (args.length === 0) {
292
- printKeyValue('Current directory', state.projectDir || 'Not set');
293
- printInfo('Use /project <directory> to change');
294
- return;
295
- }
861
+ console.log();
862
+ console.log(theme.primary(` ${prompt}`));
863
+ console.log(theme.dim(' (Press Enter to skip, or type your guidance)'));
864
+ console.log();
296
865
 
297
- const dir = path.resolve(args[0]);
298
- state.projectDir = dir;
299
- printSuccess(`Project directory set to: ${dir}`);
866
+ process.stdout.write(' > ');
867
+
868
+ rl.once('line', (answer) => {
869
+ rl.close();
870
+ resolve(answer.trim());
871
+ });
872
+ });
300
873
  }
301
874
 
302
875
  /**
303
- * Handle /resume command
876
+ * Discovered project context from docs/ folder
304
877
  */
305
- async function handleResume(_args: string[], state: SessionState): Promise<void> {
306
- if (!state.projectDir) {
307
- printError('No project directory set. Use /project <dir> first.');
308
- return;
309
- }
878
+ interface DiscoveredProject {
879
+ found: boolean;
880
+ name?: string;
881
+ idea?: string;
882
+ plan?: string;
883
+ planFile?: string;
884
+ readme?: string;
885
+ language?: OutputLanguage;
886
+ hasCode: boolean;
887
+ codeFiles: string[];
888
+ }
310
889
 
311
- if (!state.authenticated) {
312
- printError('Not authenticated. Run "popeye-cli auth login" first.');
313
- return;
314
- }
890
+ /**
891
+ * Discover project context from docs/ and codebase
892
+ */
893
+ async function discoverProjectContext(projectDir: string): Promise<DiscoveredProject> {
894
+ const result: DiscoveredProject = {
895
+ found: false,
896
+ hasCode: false,
897
+ codeFiles: [],
898
+ };
315
899
 
316
- const status = await getWorkflowStatus(state.projectDir);
900
+ const docsDir = path.join(projectDir, 'docs');
317
901
 
318
- if (!status.exists) {
319
- printError('No project found to resume');
320
- return;
902
+ // Try to read plan files
903
+ const planFiles = ['PLAN.md', 'PLAN-DRAFT.md'];
904
+ for (const planFile of planFiles) {
905
+ try {
906
+ const planPath = path.join(docsDir, planFile);
907
+ const content = await fs.readFile(planPath, 'utf-8');
908
+ result.plan = content;
909
+ result.planFile = planFile;
910
+ result.found = true;
911
+
912
+ // Try to extract project name from plan
913
+ const nameMatch = content.match(/^#\s*(?:Development Plan|Project):\s*(.+)$/mi) ||
914
+ content.match(/^#\s*(.+)$/m);
915
+ if (nameMatch) {
916
+ result.name = nameMatch[1].replace(/Development Plan/i, '').trim();
917
+ }
918
+
919
+ // Try to extract idea/overview from plan
920
+ const overviewMatch = content.match(/(?:Overview|Summary|Description)[:\s]*\n+([\s\S]*?)(?=\n#|\n\*\*|$)/i);
921
+ if (overviewMatch) {
922
+ result.idea = overviewMatch[1].trim().slice(0, 500);
923
+ }
924
+ break;
925
+ } catch {
926
+ // File doesn't exist, continue
927
+ }
321
928
  }
322
929
 
323
- printInfo('Resuming workflow...');
324
- console.log();
930
+ // Try to read README
931
+ try {
932
+ const readmePath = path.join(projectDir, 'README.md');
933
+ const content = await fs.readFile(readmePath, 'utf-8');
934
+ result.readme = content;
935
+ result.found = true;
325
936
 
326
- const result = await resumeWorkflow(state.projectDir, {
327
- onProgress: (phase, message) => {
328
- console.log(` [${phase}] ${message}`);
329
- },
330
- });
937
+ // Extract project name from README if not already found
938
+ if (!result.name) {
939
+ const nameMatch = content.match(/^#\s*(.+)$/m);
940
+ if (nameMatch) {
941
+ result.name = nameMatch[1].trim();
942
+ }
943
+ }
331
944
 
332
- if (result.success) {
333
- printSuccess('Workflow completed!');
334
- } else {
335
- printError(result.error || 'Workflow failed');
945
+ // Extract idea from README if not found in plan
946
+ if (!result.idea) {
947
+ const lines = content.split('\n').slice(1, 10).join('\n').trim();
948
+ if (lines.length > 20) {
949
+ result.idea = lines.slice(0, 500);
950
+ }
951
+ }
952
+ } catch {
953
+ // README doesn't exist
336
954
  }
337
- }
338
955
 
339
- /**
340
- * Handle /clear command
341
- */
342
- async function handleClear(_args: string[], _state: SessionState): Promise<void> {
343
- clearConsole();
344
- }
956
+ // Scan for code files to detect language
957
+ try {
958
+ const files = await fs.readdir(projectDir, { recursive: true });
959
+ const codeExtensions = {
960
+ python: ['.py'],
961
+ typescript: ['.ts', '.tsx', '.js', '.jsx'],
962
+ };
345
963
 
346
- /**
347
- * Handle /exit command
348
- */
349
- async function handleExit(_args: string[], _state: SessionState): Promise<void> {
350
- printInfo('Goodbye!');
351
- process.exit(0);
352
- }
964
+ let pyCount = 0;
965
+ let tsCount = 0;
353
966
 
354
- /**
355
- * Handle project idea input
967
+ for (const file of files) {
968
+ const fileName = String(file);
969
+ if (fileName.includes('node_modules') || fileName.includes('.git') || fileName.includes('__pycache__')) {
970
+ continue;
971
+ }
972
+
973
+ if (codeExtensions.python.some(ext => fileName.endsWith(ext))) {
974
+ pyCount++;
975
+ result.codeFiles.push(fileName);
976
+ }
977
+ if (codeExtensions.typescript.some(ext => fileName.endsWith(ext))) {
978
+ tsCount++;
979
+ result.codeFiles.push(fileName);
980
+ }
981
+ }
982
+
983
+ result.hasCode = result.codeFiles.length > 0;
984
+
985
+ // Determine language from code files
986
+ if (pyCount > tsCount) {
987
+ result.language = 'python';
988
+ } else if (tsCount > 0) {
989
+ result.language = 'typescript';
990
+ }
991
+ } catch {
992
+ // Can't read directory
993
+ }
994
+
995
+ return result;
996
+ }
997
+
998
+ /**
999
+ * Handle /resume command
356
1000
  */
357
- async function handleIdea(idea: string, state: SessionState): Promise<void> {
358
- if (!state.authenticated) {
359
- printError('Not authenticated. Run /auth to check status.');
1001
+ async function handleResume(state: SessionState, args: string[]): Promise<void> {
1002
+ if (!state.claudeAuth || !state.openaiAuth) {
1003
+ printError('Authentication required. Run /auth first.');
360
1004
  return;
361
1005
  }
362
1006
 
363
- printSection('Creating Project');
364
- printKeyValue('Idea', idea);
365
- printKeyValue('Language', state.language);
366
- printKeyValue('Model', state.model);
1007
+ // Discover all projects (registered + scanned in current directory)
367
1008
  console.log();
1009
+ printInfo('Scanning for projects...');
1010
+
1011
+ const { all: allProjects } = await discoverProjects(state.projectDir || process.cwd());
1012
+
1013
+ // If projects found, let user select one
1014
+ if (allProjects.length > 0) {
1015
+ console.log();
1016
+ console.log(theme.primary.bold(' Found Projects:'));
1017
+ console.log();
1018
+
1019
+ // Show project list with numbers
1020
+ const displayProjects = allProjects.slice(0, 10); // Limit to 10
1021
+ for (let i = 0; i < displayProjects.length; i++) {
1022
+ const project = displayProjects[i];
1023
+ const info = formatProjectForDisplay(project);
1024
+ const statusColor = project.status === 'complete' ? theme.success :
1025
+ project.status === 'failed' ? theme.error :
1026
+ project.status === 'in-progress' ? theme.warning : theme.dim;
1027
+
1028
+ console.log(` ${theme.primary(`${i + 1}.`)} ${theme.secondary(info.name)}`);
1029
+ console.log(` ${statusColor(info.status)} ${theme.dim('|')} ${info.age}`);
1030
+ console.log(` ${theme.dim(info.path)}`);
1031
+ if (project.idea) {
1032
+ console.log(` ${theme.dim(project.idea.slice(0, 60))}${project.idea.length > 60 ? '...' : ''}`);
1033
+ }
1034
+ console.log();
1035
+ }
1036
+
1037
+ if (allProjects.length > 10) {
1038
+ console.log(theme.dim(` ... and ${allProjects.length - 10} more projects`));
1039
+ console.log();
1040
+ }
1041
+
1042
+ // Let user select
1043
+ const selection = await promptSelection(
1044
+ 'Select a project to resume:',
1045
+ [
1046
+ ...displayProjects.map((p, i) => ({
1047
+ value: String(i),
1048
+ label: `${p.name} (${formatProjectForDisplay(p).age})`,
1049
+ })),
1050
+ { value: 'scan', label: 'Scan for more projects...' },
1051
+ { value: 'cancel', label: 'Cancel' },
1052
+ ],
1053
+ '0'
1054
+ );
1055
+
1056
+ if (selection === 'cancel') {
1057
+ printInfo('Cancelled');
1058
+ return;
1059
+ }
1060
+
1061
+ if (selection === 'scan') {
1062
+ // Scan deeper in current directory
1063
+ printInfo('Scanning subdirectories...');
1064
+ const { all: deepScan } = await discoverProjects(state.projectDir || process.cwd());
1065
+ if (deepScan.length === allProjects.length) {
1066
+ printWarning('No additional projects found');
1067
+ } else {
1068
+ printSuccess(`Found ${deepScan.length - allProjects.length} additional projects`);
1069
+ }
1070
+ // Recursively call handleResume to show updated list
1071
+ await handleResume(state, args);
1072
+ return;
1073
+ }
1074
+
1075
+ const selectedIndex = parseInt(selection, 10);
1076
+ const selectedProject = displayProjects[selectedIndex];
1077
+
1078
+ if (!selectedProject) {
1079
+ printError('Invalid selection');
1080
+ return;
1081
+ }
1082
+
1083
+ // Set the project directory and continue
1084
+ state.projectDir = selectedProject.path;
1085
+ console.log();
1086
+ printInfo(`Selected: ${selectedProject.name}`);
1087
+ }
1088
+
1089
+ // Now check for formal project state at the selected/current directory
1090
+ if (!state.projectDir) {
1091
+ printError('No project directory set');
1092
+ return;
1093
+ }
1094
+
1095
+ const status = await getWorkflowStatus(state.projectDir);
1096
+
1097
+ if (status.exists && status.state) {
1098
+ // Formal project state exists - analyze actual progress before resuming
1099
+ // Update session state to reflect project's language (preserves language on resume)
1100
+ state.language = status.state.language;
1101
+
1102
+ // Get detailed progress analysis
1103
+ const progressAnalysis = await analyzeProjectProgress(state.projectDir);
1104
+ const verification = await verifyProjectCompletion(state.projectDir);
1105
+
1106
+ console.log();
1107
+ console.log(theme.primary.bold(' Project Status:'));
1108
+ console.log(` ${theme.dim('Name:')} ${status.state.name}`);
1109
+ console.log(` ${theme.dim('Language:')} ${theme.primary(status.state.language)}`);
1110
+ console.log(` ${theme.dim('Phase:')} ${theme.primary(status.state.phase)}`);
1111
+ console.log(` ${theme.dim('Status:')} ${status.state.status}`);
1112
+
1113
+ // Show detailed progress comparison
1114
+ console.log();
1115
+ console.log(theme.primary.bold(' Progress Analysis:'));
1116
+ console.log(` ${theme.dim('Milestones:')} ${progressAnalysis.completedMilestones}/${progressAnalysis.totalMilestones} complete`);
1117
+ console.log(` ${theme.dim('Tasks:')} ${progressAnalysis.completedTasks}/${progressAnalysis.totalTasks} complete (${progressAnalysis.percentComplete}%)`);
1118
+
1119
+ if (progressAnalysis.inProgressTasks > 0) {
1120
+ console.log(` ${theme.dim('In Progress:')} ${theme.warning(String(progressAnalysis.inProgressTasks))} task(s)`);
1121
+ }
1122
+ if (progressAnalysis.failedTasks > 0) {
1123
+ console.log(` ${theme.dim('Failed:')} ${theme.error(String(progressAnalysis.failedTasks))} task(s)`);
1124
+ }
1125
+ if (progressAnalysis.pendingTasks > 0) {
1126
+ console.log(` ${theme.dim('Pending:')} ${progressAnalysis.pendingTasks} task(s)`);
1127
+ }
1128
+
1129
+ // Show plan file comparison
1130
+ if (progressAnalysis.planTaskCount > 0) {
1131
+ console.log();
1132
+ console.log(theme.primary.bold(' Plan Comparison (from docs/PLAN.md):'));
1133
+ console.log(` ${theme.dim('Plan Tasks:')} ${progressAnalysis.planTaskCount} tasks found in plan`);
1134
+ console.log(` ${theme.dim('State Tasks:')} ${progressAnalysis.totalTasks} tasks in state`);
1135
+
1136
+ // Show plan mismatch warning (critical - plan has more tasks than state)
1137
+ if (progressAnalysis.planMismatch) {
1138
+ console.log();
1139
+ console.log(theme.error.bold(' CRITICAL: Plan Mismatch Detected!'));
1140
+ console.log(theme.error(` The plan file has ${progressAnalysis.planTaskCount} tasks but state only has ${progressAnalysis.totalTasks}.`));
1141
+ console.log(theme.error(` This means the plan was not fully parsed into tasks.`));
1142
+ console.log(theme.error(` True progress: ${progressAnalysis.completedTasks}/${progressAnalysis.planTaskCount} tasks (${progressAnalysis.percentComplete}%)`));
1143
+
1144
+ // Show some missing tasks
1145
+ if (progressAnalysis.missingFromState.length > 0) {
1146
+ console.log();
1147
+ console.log(theme.warning(' Tasks in plan but missing from state:'));
1148
+ for (const task of progressAnalysis.missingFromState.slice(0, 8)) {
1149
+ console.log(` ${theme.dim('-')} ${task.slice(0, 70)}${task.length > 70 ? '...' : ''}`);
1150
+ }
1151
+ if (progressAnalysis.missingFromState.length > 8) {
1152
+ console.log(` ${theme.dim(`... and ${progressAnalysis.missingFromState.length - 8} more tasks`)}`);
1153
+ }
1154
+ }
1155
+
1156
+ console.log();
1157
+ console.log(theme.secondary(' The plan needs to be re-parsed to capture all tasks.'));
1158
+ console.log(theme.secondary(' Consider running the workflow again or manually adding tasks.'));
1159
+ }
1160
+ } else if (progressAnalysis.planParseError) {
1161
+ console.log();
1162
+ console.log(theme.dim(` Plan file: ${progressAnalysis.planParseError}`));
1163
+ }
1164
+
1165
+ // Check for status mismatch (status says complete but state tasks are incomplete)
1166
+ if (progressAnalysis.statusMismatch && !progressAnalysis.planMismatch) {
1167
+ console.log();
1168
+ console.log(theme.warning.bold(' WARNING: Status Mismatch Detected!'));
1169
+ console.log(theme.warning(` Project status says '${status.state.status}' but work is incomplete.`));
1170
+ console.log(theme.warning(` ${progressAnalysis.progressSummary}`));
1171
+ console.log(theme.secondary(' Will reset status and continue execution.'));
1172
+ }
1173
+
1174
+ // Show next items to work on
1175
+ if (progressAnalysis.nextMilestone || progressAnalysis.nextTask) {
1176
+ console.log();
1177
+ console.log(theme.secondary(' Next Up:'));
1178
+ if (progressAnalysis.nextMilestone) {
1179
+ console.log(` ${theme.dim('Milestone:')} ${progressAnalysis.nextMilestone.name}`);
1180
+ }
1181
+ if (progressAnalysis.nextTask) {
1182
+ console.log(` ${theme.dim('Task:')} ${progressAnalysis.nextTask.name}`);
1183
+ }
1184
+ }
1185
+
1186
+ // Show incomplete milestones
1187
+ if (progressAnalysis.incompleteMilestones.length > 0 && !verification.isComplete) {
1188
+ console.log();
1189
+ console.log(theme.secondary(' Remaining Milestones:'));
1190
+ for (const m of progressAnalysis.incompleteMilestones.slice(0, 5)) {
1191
+ console.log(` ${theme.dim('-')} ${m.name} (${m.tasksRemaining} tasks remaining)`);
1192
+ }
1193
+ if (progressAnalysis.incompleteMilestones.length > 5) {
1194
+ console.log(` ${theme.dim(`... and ${progressAnalysis.incompleteMilestones.length - 5} more`)}`);
1195
+ }
1196
+ }
1197
+
1198
+ if (status.state.consensusHistory && status.state.consensusHistory.length > 0) {
1199
+ const lastConsensus = status.state.consensusHistory[status.state.consensusHistory.length - 1];
1200
+ console.log();
1201
+ console.log(theme.secondary(' Consensus History:'));
1202
+ console.log(` ${theme.dim('Last Score:')} ${lastConsensus.result.score}%`);
1203
+ console.log(` ${theme.dim('Iterations:')} ${status.state.consensusHistory.length}`);
1204
+
1205
+ // Show last concerns
1206
+ if (lastConsensus.result.concerns && lastConsensus.result.concerns.length > 0) {
1207
+ console.log();
1208
+ console.log(theme.secondary(' Last Concerns:'));
1209
+ for (const concern of lastConsensus.result.concerns.slice(0, 3)) {
1210
+ console.log(` ${theme.dim('-')} ${concern.slice(0, 80)}${concern.length > 80 ? '...' : ''}`);
1211
+ }
1212
+ }
1213
+ }
1214
+
1215
+ if (status.state.error) {
1216
+ console.log();
1217
+ console.log(theme.error(` Error: ${status.state.error}`));
1218
+ }
1219
+
1220
+ // If project says complete but isn't, inform user we'll continue
1221
+ if (verification.isComplete) {
1222
+ console.log();
1223
+ printSuccess('Project is fully complete!');
1224
+ printInfo(`All ${progressAnalysis.totalTasks} tasks across ${progressAnalysis.totalMilestones} milestones are done.`);
1225
+ return;
1226
+ }
1227
+
1228
+ // Check if user provided context as argument
1229
+ let additionalContext = args.join(' ').trim();
1230
+
1231
+ // If no context provided, ask if they want to add guidance
1232
+ if (!additionalContext) {
1233
+ console.log();
1234
+ const wantsContext = await promptYesNo(
1235
+ theme.primary('Would you like to add guidance before resuming?'),
1236
+ false
1237
+ );
1238
+
1239
+ if (wantsContext) {
1240
+ additionalContext = await promptForContext(
1241
+ 'What guidance would you like to give? (e.g., "Focus on simplicity", "Use SQLite instead of PostgreSQL")'
1242
+ );
1243
+ }
1244
+ }
1245
+
1246
+ console.log();
1247
+ printInfo('Resuming workflow...');
1248
+ if (additionalContext) {
1249
+ console.log(` ${theme.dim('With guidance:')} ${additionalContext.slice(0, 60)}${additionalContext.length > 60 ? '...' : ''}`);
1250
+ }
1251
+ console.log();
1252
+
1253
+ const result = await resumeWorkflow(state.projectDir, {
1254
+ consensusConfig: {
1255
+ reviewer: state.reviewer,
1256
+ arbitrator: state.arbitrator,
1257
+ enableArbitration: state.enableArbitration,
1258
+ },
1259
+ additionalContext,
1260
+ onProgress: (phase, message) => {
1261
+ console.log(` ${theme.dim(`[${phase}]`)} ${message}`);
1262
+ },
1263
+ });
1264
+
1265
+ console.log();
1266
+ if (result.success) {
1267
+ // Update README with project description on completion
1268
+ await updateReadmeOnCompletion(
1269
+ state.projectDir,
1270
+ status.state.name,
1271
+ status.state.idea,
1272
+ status.state.language
1273
+ );
1274
+
1275
+ printSuccess('Workflow completed!');
1276
+ console.log(` ${theme.dim('Location:')} ${state.projectDir}`);
1277
+ } else {
1278
+ printError(result.error || 'Workflow failed');
1279
+ printInfo('You can run /resume again with additional guidance');
1280
+ }
1281
+ return;
1282
+ }
1283
+
1284
+ // No formal project state - try to discover context from docs/
1285
+ printInfo('No project state found in selected directory. Scanning for project context...');
1286
+ console.log();
1287
+
1288
+ const discovered = await discoverProjectContext(state.projectDir);
1289
+
1290
+ if (!discovered.found) {
1291
+ console.log(theme.secondary(' No project context found in this directory.'));
1292
+ console.log();
1293
+ console.log(theme.dim(' To start a new project, simply type your idea:'));
1294
+ console.log(theme.dim(' Example: "Build a REST API for task management"'));
1295
+ console.log();
1296
+ console.log(theme.dim(' Or navigate to a directory with existing plans:'));
1297
+ console.log(theme.dim(' - docs/PLAN.md or docs/PLAN-DRAFT.md'));
1298
+ console.log(theme.dim(' - README.md'));
1299
+ return;
1300
+ }
1301
+
1302
+ // Show what we discovered
1303
+ console.log(theme.primary.bold(' Discovered Project Context:'));
1304
+ console.log();
1305
+
1306
+ if (discovered.name) {
1307
+ console.log(` ${theme.dim('Name:')} ${discovered.name}`);
1308
+ }
1309
+
1310
+ if (discovered.language) {
1311
+ console.log(` ${theme.dim('Language:')} ${theme.primary(discovered.language)}`);
1312
+ }
1313
+
1314
+ if (discovered.planFile) {
1315
+ console.log(` ${theme.dim('Plan:')} docs/${discovered.planFile}`);
1316
+ }
1317
+
1318
+ if (discovered.hasCode) {
1319
+ console.log(` ${theme.dim('Code:')} ${discovered.codeFiles.length} source files found`);
1320
+ }
1321
+
1322
+ if (discovered.idea) {
1323
+ console.log();
1324
+ console.log(theme.secondary(' Project Overview:'));
1325
+ const ideaLines = discovered.idea.split('\n').slice(0, 4);
1326
+ for (const line of ideaLines) {
1327
+ console.log(` ${theme.dim(line.slice(0, 80))}`);
1328
+ }
1329
+ if (discovered.idea.split('\n').length > 4) {
1330
+ console.log(theme.dim(' ...'));
1331
+ }
1332
+ }
1333
+
1334
+ if (discovered.plan) {
1335
+ // Show plan summary
1336
+ console.log();
1337
+ console.log(theme.secondary(' Plan Summary:'));
1338
+ const planLines = discovered.plan.split('\n').filter(l => l.trim().startsWith('#') || l.trim().startsWith('-')).slice(0, 8);
1339
+ for (const line of planLines) {
1340
+ console.log(` ${theme.dim(line.slice(0, 80))}`);
1341
+ }
1342
+ }
1343
+
1344
+ console.log();
1345
+
1346
+ // Ask user what they want to do
1347
+ const action = await promptSelection(
1348
+ 'What would you like to do?',
1349
+ [
1350
+ { value: 'continue', label: 'Continue with this plan - use existing and continue development' },
1351
+ { value: 'refine', label: 'Refine the plan - review and improve with consensus' },
1352
+ { value: 'new', label: 'Start fresh - provide a new idea' },
1353
+ { value: 'cancel', label: 'Cancel' },
1354
+ ],
1355
+ 'continue'
1356
+ );
1357
+
1358
+ if (action === 'cancel') {
1359
+ printInfo('Cancelled');
1360
+ return;
1361
+ }
1362
+
1363
+ if (action === 'new') {
1364
+ console.log();
1365
+ printInfo('Type your project idea to start a new workflow');
1366
+ return;
1367
+ }
1368
+
1369
+ // Get additional context/guidance
1370
+ let additionalContext = args.join(' ').trim();
1371
+
1372
+ if (!additionalContext && action === 'refine') {
1373
+ additionalContext = await promptForContext(
1374
+ 'What changes or improvements would you like? (e.g., "Add authentication", "Simplify the architecture")'
1375
+ );
1376
+ } else if (!additionalContext) {
1377
+ console.log();
1378
+ const wantsContext = await promptYesNo(
1379
+ theme.primary('Would you like to add any guidance?'),
1380
+ false
1381
+ );
1382
+
1383
+ if (wantsContext) {
1384
+ additionalContext = await promptForContext(
1385
+ 'What guidance would you like to give?'
1386
+ );
1387
+ }
1388
+ }
1389
+
1390
+ // Create project spec from discovered context
1391
+ const projectName = discovered.name ||
1392
+ path.basename(state.projectDir)
1393
+ .toLowerCase()
1394
+ .replace(/[^a-z0-9]/g, '-')
1395
+ .substring(0, 30) ||
1396
+ 'my-project';
1397
+
1398
+ const spec: ProjectSpec = {
1399
+ idea: discovered.idea || discovered.plan?.slice(0, 500) || `Continue developing ${projectName}`,
1400
+ name: projectName,
1401
+ language: discovered.language || state.language,
1402
+ openaiModel: state.model,
1403
+ outputDir: state.projectDir,
1404
+ };
1405
+
1406
+ console.log();
1407
+ printInfo(`Starting workflow for "${projectName}"...`);
1408
+ if (additionalContext) {
1409
+ console.log(` ${theme.dim('With guidance:')} ${additionalContext.slice(0, 60)}${additionalContext.length > 60 ? '...' : ''}`);
1410
+ }
1411
+ console.log();
1412
+
1413
+ // Run the workflow
1414
+ const result = await runWorkflow(spec, {
1415
+ projectDir: state.projectDir,
1416
+ consensusConfig: {
1417
+ reviewer: state.reviewer,
1418
+ arbitrator: state.arbitrator,
1419
+ enableArbitration: state.enableArbitration,
1420
+ },
1421
+ onProgress: (phase, message) => {
1422
+ console.log(` ${theme.dim(`[${phase}]`)} ${message}`);
1423
+ },
1424
+ });
1425
+
1426
+ console.log();
1427
+ if (result.success) {
1428
+ // Update README with project description on completion
1429
+ if (state.projectDir) {
1430
+ await updateReadmeOnCompletion(
1431
+ state.projectDir,
1432
+ spec.name || 'my-project',
1433
+ spec.idea,
1434
+ spec.language
1435
+ );
1436
+ }
368
1437
 
369
- // Generate project name
370
- const projectName = idea
1438
+ printSuccess('Workflow completed!');
1439
+ console.log(` ${theme.dim('Location:')} ${state.projectDir}`);
1440
+ } else {
1441
+ printError(result.error || 'Workflow failed');
1442
+ printInfo('You can run /resume again with additional guidance');
1443
+ }
1444
+ }
1445
+
1446
+ /**
1447
+ * Generate a meaningful project name from an idea
1448
+ * Extracts key nouns and creates a kebab-case name
1449
+ */
1450
+ function generateProjectName(idea: string): string {
1451
+ // Common words to filter out
1452
+ const stopWords = new Set([
1453
+ 'a', 'an', 'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
1454
+ 'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'were', 'been',
1455
+ 'be', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
1456
+ 'should', 'may', 'might', 'must', 'shall', 'can', 'need', 'dare', 'ought',
1457
+ 'used', 'create', 'build', 'make', 'develop', 'write', 'implement',
1458
+ 'want', 'like', 'please', 'help', 'me', 'i', 'my', 'we', 'our', 'you',
1459
+ 'your', 'that', 'which', 'who', 'what', 'where', 'when', 'why', 'how',
1460
+ 'this', 'these', 'those', 'it', 'its', 'simple', 'basic', 'new',
1461
+ ]);
1462
+
1463
+ // Extract meaningful words
1464
+ const words = idea
371
1465
  .toLowerCase()
372
- .replace(/[^a-z0-9\s]/g, '')
1466
+ .replace(/[^a-z0-9\s]/g, ' ')
373
1467
  .split(/\s+/)
374
- .slice(0, 3)
375
- .join('-')
376
- .substring(0, 30) || 'my-project';
1468
+ .filter(word => word.length > 2 && !stopWords.has(word));
1469
+
1470
+ // Take first 2-3 meaningful words
1471
+ const nameWords = words.slice(0, 3);
1472
+
1473
+ if (nameWords.length === 0) {
1474
+ // Fallback: use first words from original idea
1475
+ const fallback = idea
1476
+ .toLowerCase()
1477
+ .replace(/[^a-z0-9\s]/g, '')
1478
+ .split(/\s+/)
1479
+ .filter(w => w.length > 0)
1480
+ .slice(0, 2);
1481
+ return fallback.join('-') || 'my-project';
1482
+ }
1483
+
1484
+ return nameWords.join('-').substring(0, 40);
1485
+ }
1486
+
1487
+ /**
1488
+ * Update README.md with project description and usage instructions
1489
+ */
1490
+ async function updateReadmeOnCompletion(
1491
+ projectDir: string,
1492
+ projectName: string,
1493
+ idea: string,
1494
+ language: OutputLanguage
1495
+ ): Promise<void> {
1496
+ const readmePath = path.join(projectDir, 'README.md');
1497
+
1498
+ try {
1499
+ // Read existing README
1500
+ let content = await fs.readFile(readmePath, 'utf-8');
1501
+
1502
+ // Check if it still has the placeholder description
1503
+ if (content.includes('Generated by Popeye CLI')) {
1504
+ // Generate a better description based on the idea
1505
+ const description = `${idea}\n\nThis project was automatically generated and implemented using [Popeye CLI](https://github.com/popeye-cli).`;
1506
+
1507
+ // Replace the placeholder
1508
+ content = content.replace(
1509
+ /Generated by Popeye CLI/g,
1510
+ description
1511
+ );
1512
+
1513
+ // Add a "Getting Started" section if it doesn't exist
1514
+ if (!content.includes('## Getting Started')) {
1515
+ const gettingStarted = language === 'python'
1516
+ ? `
1517
+ ## Getting Started
1518
+
1519
+ 1. Create and activate a virtual environment:
1520
+ \`\`\`bash
1521
+ python -m venv venv
1522
+ source venv/bin/activate # On Windows: venv\\Scripts\\activate
1523
+ \`\`\`
1524
+
1525
+ 2. Install dependencies:
1526
+ \`\`\`bash
1527
+ pip install -e ".[dev]"
1528
+ \`\`\`
1529
+
1530
+ 3. Run the application:
1531
+ \`\`\`bash
1532
+ python -m src.${projectName.replace(/-/g, '_')}.main
1533
+ \`\`\`
1534
+ `
1535
+ : `
1536
+ ## Getting Started
1537
+
1538
+ 1. Install dependencies:
1539
+ \`\`\`bash
1540
+ npm install
1541
+ \`\`\`
1542
+
1543
+ 2. Build the project:
1544
+ \`\`\`bash
1545
+ npm run build
1546
+ \`\`\`
1547
+
1548
+ 3. Run the application:
1549
+ \`\`\`bash
1550
+ npm start
1551
+ \`\`\`
1552
+ `;
1553
+
1554
+ // Insert before "## Development" or at the end
1555
+ if (content.includes('## Development')) {
1556
+ content = content.replace('## Development', gettingStarted + '\n## Development');
1557
+ } else {
1558
+ content += gettingStarted;
1559
+ }
1560
+ }
1561
+
1562
+ await fs.writeFile(readmePath, content, 'utf-8');
1563
+ }
1564
+ } catch {
1565
+ // Silently ignore if README doesn't exist or can't be updated
1566
+ }
1567
+ }
1568
+
1569
+ /**
1570
+ * Handle project idea input
1571
+ */
1572
+ async function handleIdea(idea: string, state: SessionState): Promise<void> {
1573
+ const cwd = state.projectDir || process.cwd();
377
1574
 
378
- const path = await import('node:path');
379
- const projectDir = path.join(state.projectDir || process.cwd(), projectName);
1575
+ // Check for existing Popeye projects in the current directory
1576
+ const { all: existingProjects } = await discoverProjects(cwd);
1577
+ const localProjects = existingProjects.filter(p => p.path.startsWith(cwd));
1578
+
1579
+ if (localProjects.length > 0) {
1580
+ console.log();
1581
+ printWarning('Existing Popeye projects found in this directory:');
1582
+ console.log();
1583
+
1584
+ for (const project of localProjects.slice(0, 5)) {
1585
+ const display = formatProjectForDisplay(project);
1586
+ console.log(` ${theme.primary(display.name)} ${theme.dim(`(${display.age})`)}`);
1587
+ console.log(` ${theme.dim(display.status)} - ${theme.dim(project.path)}`);
1588
+ console.log();
1589
+ }
1590
+
1591
+ printInfo('Consider running /resume to continue an existing project.');
1592
+ printInfo('To start a new project anyway, run: /new ' + idea);
1593
+ return;
1594
+ }
1595
+
1596
+ if (!state.claudeAuth || !state.openaiAuth) {
1597
+ console.log();
1598
+ printError('Authentication required');
1599
+ printInfo('Running authentication flow...');
1600
+ console.log();
1601
+
1602
+ const authenticated = await ensureAuthentication(state);
1603
+ if (!authenticated) {
1604
+ printWarning('Skipping project creation - authentication incomplete');
1605
+ return;
1606
+ }
1607
+ }
1608
+
1609
+ // Generate a meaningful project name
1610
+ const projectName = generateProjectName(idea);
1611
+ const projectDir = path.join(cwd, projectName);
1612
+
1613
+ console.log();
1614
+ console.log(theme.primary.bold(' Creating Project'));
1615
+ console.log(` ${theme.dim('Idea:')} ${idea}`);
1616
+ console.log(` ${theme.dim('Name:')} ${theme.primary(projectName)}`);
1617
+ console.log(` ${theme.dim('Language:')} ${theme.primary(state.language)}`);
1618
+ console.log(` ${theme.dim('Model:')} ${theme.secondary(state.model)}`);
1619
+ console.log();
380
1620
 
381
1621
  const spec: ProjectSpec = {
382
1622
  idea,
383
1623
  name: projectName,
384
1624
  language: state.language,
385
1625
  openaiModel: state.model,
386
- outputDir: state.projectDir || process.cwd(),
1626
+ outputDir: cwd,
387
1627
  };
388
1628
 
389
1629
  // Generate scaffold
390
1630
  startSpinner('Creating project structure...');
391
- const scaffoldResult = await generateProject(spec, state.projectDir || process.cwd());
1631
+ const scaffoldResult = await generateProject(spec, cwd);
392
1632
 
393
1633
  if (!scaffoldResult.success) {
394
1634
  failSpinner('Scaffolding failed');
@@ -398,21 +1638,206 @@ async function handleIdea(idea: string, state: SessionState): Promise<void> {
398
1638
 
399
1639
  succeedSpinner(`Created ${scaffoldResult.filesCreated.length} files`);
400
1640
 
401
- // Run workflow
1641
+ // Run workflow with reviewer/arbitrator settings
1642
+ console.log();
1643
+ printInfo('Starting AI workflow...');
1644
+ console.log(` ${theme.dim('Reviewer:')} ${theme.primary(state.reviewer)}`);
1645
+ if (state.enableArbitration) {
1646
+ console.log(` ${theme.dim('Arbitrator:')} ${theme.primary(state.arbitrator)}`);
1647
+ }
1648
+ console.log();
1649
+
402
1650
  const workflowResult = await runWorkflow(spec, {
403
1651
  projectDir,
1652
+ consensusConfig: {
1653
+ reviewer: state.reviewer,
1654
+ arbitrator: state.arbitrator,
1655
+ enableArbitration: state.enableArbitration,
1656
+ geminiModel: state.geminiModel,
1657
+ },
404
1658
  onProgress: (phase, message) => {
405
- console.log(` [${phase}] ${message}`);
1659
+ console.log(` ${theme.dim(`[${phase}]`)} ${message}`);
406
1660
  },
407
1661
  });
408
1662
 
409
1663
  stopSpinner();
410
1664
 
1665
+ console.log();
411
1666
  if (workflowResult.success) {
1667
+ // Update README with project description
1668
+ await updateReadmeOnCompletion(projectDir, projectName, idea, state.language);
1669
+
412
1670
  printSuccess('Project created successfully!');
413
- printKeyValue('Location', projectDir);
1671
+ console.log(` ${theme.dim('Location:')} ${projectDir}`);
414
1672
  state.projectDir = projectDir;
415
1673
  } else {
416
1674
  printError(workflowResult.error || 'Workflow failed');
417
1675
  }
418
1676
  }
1677
+
1678
+ /**
1679
+ * Handle /new command - force create a new project (skips existing project check)
1680
+ */
1681
+ async function handleNewProject(idea: string, state: SessionState): Promise<void> {
1682
+ if (!state.claudeAuth || !state.openaiAuth) {
1683
+ console.log();
1684
+ printError('Authentication required');
1685
+ printInfo('Running authentication flow...');
1686
+ console.log();
1687
+
1688
+ const authenticated = await ensureAuthentication(state);
1689
+ if (!authenticated) {
1690
+ printWarning('Skipping project creation - authentication incomplete');
1691
+ return;
1692
+ }
1693
+ }
1694
+
1695
+ const cwd = state.projectDir || process.cwd();
1696
+
1697
+ // Generate a meaningful project name
1698
+ const projectName = generateProjectName(idea);
1699
+ const projectDir = path.join(cwd, projectName);
1700
+
1701
+ console.log();
1702
+ console.log(theme.primary.bold(' Creating New Project'));
1703
+ console.log(` ${theme.dim('Idea:')} ${idea}`);
1704
+ console.log(` ${theme.dim('Name:')} ${theme.primary(projectName)}`);
1705
+ console.log(` ${theme.dim('Language:')} ${theme.primary(state.language)}`);
1706
+ console.log(` ${theme.dim('Model:')} ${theme.secondary(state.model)}`);
1707
+ console.log();
1708
+
1709
+ const spec: ProjectSpec = {
1710
+ idea,
1711
+ name: projectName,
1712
+ language: state.language,
1713
+ openaiModel: state.model,
1714
+ outputDir: cwd,
1715
+ };
1716
+
1717
+ // Generate scaffold
1718
+ startSpinner('Creating project structure...');
1719
+ const scaffoldResult = await generateProject(spec, cwd);
1720
+
1721
+ if (!scaffoldResult.success) {
1722
+ failSpinner('Scaffolding failed');
1723
+ printError(scaffoldResult.error || 'Failed to create project');
1724
+ return;
1725
+ }
1726
+
1727
+ succeedSpinner(`Created ${scaffoldResult.filesCreated.length} files`);
1728
+
1729
+ // Run workflow with reviewer/arbitrator settings
1730
+ console.log();
1731
+ printInfo('Starting AI workflow...');
1732
+ console.log(` ${theme.dim('Reviewer:')} ${theme.primary(state.reviewer)}`);
1733
+ if (state.enableArbitration) {
1734
+ console.log(` ${theme.dim('Arbitrator:')} ${theme.primary(state.arbitrator)}`);
1735
+ }
1736
+ console.log();
1737
+
1738
+ const workflowResult = await runWorkflow(spec, {
1739
+ projectDir,
1740
+ consensusConfig: {
1741
+ reviewer: state.reviewer,
1742
+ arbitrator: state.arbitrator,
1743
+ enableArbitration: state.enableArbitration,
1744
+ geminiModel: state.geminiModel,
1745
+ },
1746
+ onProgress: (phase, message) => {
1747
+ console.log(` ${theme.dim(`[${phase}]`)} ${message}`);
1748
+ },
1749
+ });
1750
+
1751
+ stopSpinner();
1752
+
1753
+ console.log();
1754
+ if (workflowResult.success) {
1755
+ // Update README with project description
1756
+ await updateReadmeOnCompletion(projectDir, projectName, idea, state.language);
1757
+
1758
+ printSuccess('Project created successfully!');
1759
+ console.log(` ${theme.dim('Location:')} ${projectDir}`);
1760
+ state.projectDir = projectDir;
1761
+ } else {
1762
+ printError(workflowResult.error || 'Workflow failed');
1763
+ }
1764
+ }
1765
+
1766
+ /**
1767
+ * Start interactive mode with auto-authentication
1768
+ */
1769
+ export async function startInteractiveMode(): Promise<void> {
1770
+ console.clear();
1771
+
1772
+ // Initialize state from saved config
1773
+ const config = await loadConfig();
1774
+ const state: SessionState = {
1775
+ projectDir: process.cwd(),
1776
+ language: config.project.default_language,
1777
+ model: config.apis.openai.model,
1778
+ geminiModel: 'gemini-2.0-flash',
1779
+ claudeAuth: false,
1780
+ openaiAuth: false,
1781
+ geminiAuth: false,
1782
+ // Load saved reviewer/arbitrator settings from config
1783
+ reviewer: config.consensus.reviewer,
1784
+ arbitrator: config.consensus.arbitrator === 'off' ? 'openai' : config.consensus.arbitrator,
1785
+ enableArbitration: config.consensus.enable_arbitration,
1786
+ };
1787
+
1788
+ // Draw header
1789
+ drawHeader();
1790
+ console.log();
1791
+
1792
+ // Show how Popeye works
1793
+ console.log(theme.secondary(' How Popeye works:'));
1794
+ console.log(theme.dim(' ├─ ') + theme.primary('Claude Code CLI') + theme.dim(' - Generates code (uses your model & MCP settings)'));
1795
+ console.log(theme.dim(' ├─ ') + theme.secondary('Reviewer (configurable)') + theme.dim(' - Reviews plans until consensus'));
1796
+ console.log(theme.dim(' └─ ') + theme.secondary('Arbitrator (optional)') + theme.dim(' - Breaks deadlocks when stuck'));
1797
+ console.log();
1798
+ console.log(theme.dim(' You can choose OpenAI or Gemini as reviewer/arbitrator during setup.'));
1799
+ console.log(theme.dim(' Plans are saved to docs/ folder in markdown format.'));
1800
+ console.log();
1801
+
1802
+ // Check and perform authentication
1803
+ const isAuthenticated = await ensureAuthentication(state);
1804
+
1805
+ if (!isAuthenticated) {
1806
+ console.log();
1807
+ printWarning('Some services are not authenticated. Some features may not work.');
1808
+ printInfo('You can authenticate later with /auth');
1809
+ } else {
1810
+ console.log();
1811
+ printSuccess('Ready! Type your project idea or /help for commands');
1812
+ }
1813
+
1814
+ console.log();
1815
+
1816
+ // Create readline interface
1817
+ const rl = readline.createInterface({
1818
+ input: process.stdin,
1819
+ output: process.stdout,
1820
+ });
1821
+
1822
+ // Input loop
1823
+ const promptUser = (): void => {
1824
+ drawInputBoxTop(state);
1825
+
1826
+ rl.question(getPrompt(), async (input) => {
1827
+ // Draw bottom of input box after user presses enter
1828
+ drawInputBoxBottom();
1829
+
1830
+ const shouldContinue = await handleInput(input, state);
1831
+
1832
+ if (shouldContinue) {
1833
+ console.log();
1834
+ promptUser();
1835
+ } else {
1836
+ rl.close();
1837
+ process.exit(0);
1838
+ }
1839
+ });
1840
+ };
1841
+
1842
+ promptUser();
1843
+ }