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