popeye-cli 1.1.0 → 1.2.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 (137) hide show
  1. package/.env.example +24 -1
  2. package/CONTRIBUTING.md +275 -0
  3. package/OPEN_SOURCE_MANIFESTO.md +172 -0
  4. package/README.md +340 -27
  5. package/dist/adapters/claude.d.ts +5 -2
  6. package/dist/adapters/claude.d.ts.map +1 -1
  7. package/dist/adapters/claude.js +239 -19
  8. package/dist/adapters/claude.js.map +1 -1
  9. package/dist/adapters/grok.d.ts +73 -0
  10. package/dist/adapters/grok.d.ts.map +1 -0
  11. package/dist/adapters/grok.js +430 -0
  12. package/dist/adapters/grok.js.map +1 -0
  13. package/dist/adapters/openai.d.ts +1 -1
  14. package/dist/adapters/openai.d.ts.map +1 -1
  15. package/dist/adapters/openai.js +6 -1
  16. package/dist/adapters/openai.js.map +1 -1
  17. package/dist/auth/grok.d.ts +73 -0
  18. package/dist/auth/grok.d.ts.map +1 -0
  19. package/dist/auth/grok.js +211 -0
  20. package/dist/auth/grok.js.map +1 -0
  21. package/dist/auth/index.d.ts +9 -6
  22. package/dist/auth/index.d.ts.map +1 -1
  23. package/dist/auth/index.js +23 -6
  24. package/dist/auth/index.js.map +1 -1
  25. package/dist/cli/commands/auth.d.ts +1 -1
  26. package/dist/cli/commands/auth.d.ts.map +1 -1
  27. package/dist/cli/commands/auth.js +79 -8
  28. package/dist/cli/commands/auth.js.map +1 -1
  29. package/dist/cli/commands/create.d.ts.map +1 -1
  30. package/dist/cli/commands/create.js +15 -4
  31. package/dist/cli/commands/create.js.map +1 -1
  32. package/dist/cli/interactive.d.ts.map +1 -1
  33. package/dist/cli/interactive.js +374 -35
  34. package/dist/cli/interactive.js.map +1 -1
  35. package/dist/config/defaults.d.ts +3 -0
  36. package/dist/config/defaults.d.ts.map +1 -1
  37. package/dist/config/defaults.js +9 -0
  38. package/dist/config/defaults.js.map +1 -1
  39. package/dist/config/index.d.ts +9 -0
  40. package/dist/config/index.d.ts.map +1 -1
  41. package/dist/config/index.js +16 -3
  42. package/dist/config/index.js.map +1 -1
  43. package/dist/config/schema.d.ts +27 -0
  44. package/dist/config/schema.d.ts.map +1 -1
  45. package/dist/config/schema.js +24 -3
  46. package/dist/config/schema.js.map +1 -1
  47. package/dist/generators/fullstack.d.ts +32 -0
  48. package/dist/generators/fullstack.d.ts.map +1 -0
  49. package/dist/generators/fullstack.js +497 -0
  50. package/dist/generators/fullstack.js.map +1 -0
  51. package/dist/generators/index.d.ts +4 -3
  52. package/dist/generators/index.d.ts.map +1 -1
  53. package/dist/generators/index.js +15 -1
  54. package/dist/generators/index.js.map +1 -1
  55. package/dist/generators/python.d.ts +17 -1
  56. package/dist/generators/python.d.ts.map +1 -1
  57. package/dist/generators/python.js +34 -21
  58. package/dist/generators/python.js.map +1 -1
  59. package/dist/generators/templates/fullstack.d.ts +113 -0
  60. package/dist/generators/templates/fullstack.d.ts.map +1 -0
  61. package/dist/generators/templates/fullstack.js +1004 -0
  62. package/dist/generators/templates/fullstack.js.map +1 -0
  63. package/dist/generators/typescript.d.ts +19 -1
  64. package/dist/generators/typescript.d.ts.map +1 -1
  65. package/dist/generators/typescript.js +37 -21
  66. package/dist/generators/typescript.js.map +1 -1
  67. package/dist/types/cli.d.ts +4 -0
  68. package/dist/types/cli.d.ts.map +1 -1
  69. package/dist/types/cli.js.map +1 -1
  70. package/dist/types/consensus.d.ts +119 -2
  71. package/dist/types/consensus.d.ts.map +1 -1
  72. package/dist/types/consensus.js +12 -1
  73. package/dist/types/consensus.js.map +1 -1
  74. package/dist/types/project.d.ts +76 -0
  75. package/dist/types/project.d.ts.map +1 -1
  76. package/dist/types/project.js +1 -1
  77. package/dist/types/project.js.map +1 -1
  78. package/dist/types/workflow.d.ts +162 -16
  79. package/dist/types/workflow.d.ts.map +1 -1
  80. package/dist/types/workflow.js +24 -1
  81. package/dist/types/workflow.js.map +1 -1
  82. package/dist/workflow/consensus.d.ts +29 -3
  83. package/dist/workflow/consensus.d.ts.map +1 -1
  84. package/dist/workflow/consensus.js +334 -27
  85. package/dist/workflow/consensus.js.map +1 -1
  86. package/dist/workflow/milestone-workflow.js +2 -2
  87. package/dist/workflow/milestone-workflow.js.map +1 -1
  88. package/dist/workflow/plan-mode.d.ts +66 -2
  89. package/dist/workflow/plan-mode.d.ts.map +1 -1
  90. package/dist/workflow/plan-mode.js +187 -11
  91. package/dist/workflow/plan-mode.js.map +1 -1
  92. package/dist/workflow/plan-storage.d.ts +252 -8
  93. package/dist/workflow/plan-storage.d.ts.map +1 -1
  94. package/dist/workflow/plan-storage.js +580 -33
  95. package/dist/workflow/plan-storage.js.map +1 -1
  96. package/dist/workflow/project-verification.js +1 -1
  97. package/dist/workflow/project-verification.js.map +1 -1
  98. package/dist/workflow/task-workflow.d.ts.map +1 -1
  99. package/dist/workflow/task-workflow.js +4 -1
  100. package/dist/workflow/task-workflow.js.map +1 -1
  101. package/dist/workflow/test-runner.d.ts +8 -0
  102. package/dist/workflow/test-runner.d.ts.map +1 -1
  103. package/dist/workflow/test-runner.js +92 -0
  104. package/dist/workflow/test-runner.js.map +1 -1
  105. package/dist/workflow/workspace-manager.d.ts +342 -0
  106. package/dist/workflow/workspace-manager.d.ts.map +1 -0
  107. package/dist/workflow/workspace-manager.js +733 -0
  108. package/dist/workflow/workspace-manager.js.map +1 -0
  109. package/package.json +1 -1
  110. package/src/adapters/claude.ts +263 -24
  111. package/src/adapters/grok.ts +492 -0
  112. package/src/adapters/openai.ts +8 -2
  113. package/src/auth/grok.ts +255 -0
  114. package/src/auth/index.ts +27 -9
  115. package/src/cli/commands/auth.ts +89 -10
  116. package/src/cli/commands/create.ts +13 -4
  117. package/src/cli/interactive.ts +424 -34
  118. package/src/config/defaults.ts +9 -0
  119. package/src/config/index.ts +17 -3
  120. package/src/config/schema.ts +25 -3
  121. package/src/generators/fullstack.ts +551 -0
  122. package/src/generators/index.ts +25 -1
  123. package/src/generators/python.ts +65 -21
  124. package/src/generators/templates/fullstack.ts +1047 -0
  125. package/src/generators/typescript.ts +69 -21
  126. package/src/types/cli.ts +4 -0
  127. package/src/types/consensus.ts +135 -3
  128. package/src/types/project.ts +82 -1
  129. package/src/types/workflow.ts +56 -2
  130. package/src/workflow/consensus.ts +461 -31
  131. package/src/workflow/milestone-workflow.ts +2 -2
  132. package/src/workflow/plan-mode.ts +238 -10
  133. package/src/workflow/plan-storage.ts +835 -35
  134. package/src/workflow/project-verification.ts +1 -1
  135. package/src/workflow/task-workflow.ts +4 -1
  136. package/src/workflow/test-runner.ts +110 -0
  137. package/src/workflow/workspace-manager.ts +912 -0
@@ -11,9 +11,11 @@ import {
11
11
  authenticateClaude,
12
12
  authenticateOpenAI,
13
13
  authenticateGemini,
14
+ authenticateGrok,
14
15
  isClaudeCLIInstalled,
15
16
  checkClaudeCLIAuth,
16
17
  checkGeminiAuth,
18
+ checkGrokAuth,
17
19
  } from '../auth/index.js';
18
20
  import {
19
21
  runWorkflow,
@@ -46,6 +48,177 @@ import {
46
48
  theme,
47
49
  } from './output.js';
48
50
 
51
+ /**
52
+ * Project-local configuration stored in popeye.md
53
+ */
54
+ interface PopeyeProjectConfig {
55
+ language: OutputLanguage;
56
+ reviewer: AIProvider;
57
+ arbitrator: AIProvider;
58
+ enableArbitration: boolean;
59
+ created: string;
60
+ lastRun: string;
61
+ projectName?: string;
62
+ description?: string;
63
+ notes?: string;
64
+ }
65
+
66
+ /**
67
+ * Read popeye.md from project directory
68
+ * Returns null if file doesn't exist
69
+ */
70
+ async function readPopeyeConfig(projectDir: string): Promise<PopeyeProjectConfig | null> {
71
+ const configPath = path.join(projectDir, 'popeye.md');
72
+
73
+ try {
74
+ const content = await fs.readFile(configPath, 'utf-8');
75
+
76
+ // Parse YAML frontmatter
77
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
78
+ if (!frontmatterMatch) {
79
+ return null;
80
+ }
81
+
82
+ const frontmatter = frontmatterMatch[1];
83
+ const config: Partial<PopeyeProjectConfig> = {};
84
+
85
+ // Parse each line of YAML
86
+ for (const line of frontmatter.split('\n')) {
87
+ const match = line.match(/^(\w+):\s*(.+)$/);
88
+ if (match) {
89
+ const [, key, value] = match;
90
+ const cleanValue = value.trim();
91
+
92
+ switch (key) {
93
+ case 'language':
94
+ if (['python', 'typescript', 'fullstack'].includes(cleanValue)) {
95
+ config.language = cleanValue as OutputLanguage;
96
+ }
97
+ break;
98
+ case 'reviewer':
99
+ if (['openai', 'gemini', 'grok'].includes(cleanValue)) {
100
+ config.reviewer = cleanValue as AIProvider;
101
+ }
102
+ break;
103
+ case 'arbitrator':
104
+ if (['openai', 'gemini', 'grok', 'off'].includes(cleanValue)) {
105
+ if (cleanValue === 'off') {
106
+ config.enableArbitration = false;
107
+ } else {
108
+ config.arbitrator = cleanValue as AIProvider;
109
+ config.enableArbitration = true;
110
+ }
111
+ }
112
+ break;
113
+ case 'created':
114
+ config.created = cleanValue;
115
+ break;
116
+ case 'lastRun':
117
+ config.lastRun = cleanValue;
118
+ break;
119
+ case 'projectName':
120
+ config.projectName = cleanValue;
121
+ break;
122
+ }
123
+ }
124
+ }
125
+
126
+ // Extract notes section if present
127
+ const notesMatch = content.match(/## Notes\n([\s\S]*?)(?=\n## |$)/);
128
+ if (notesMatch) {
129
+ config.notes = notesMatch[1].trim();
130
+ }
131
+
132
+ // Return config only if we have the essential fields
133
+ if (config.language && config.reviewer) {
134
+ return {
135
+ language: config.language,
136
+ reviewer: config.reviewer,
137
+ arbitrator: config.arbitrator || 'gemini',
138
+ enableArbitration: config.enableArbitration ?? true,
139
+ created: config.created || new Date().toISOString(),
140
+ lastRun: config.lastRun || new Date().toISOString(),
141
+ projectName: config.projectName,
142
+ description: config.description,
143
+ notes: config.notes,
144
+ };
145
+ }
146
+
147
+ return null;
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Write popeye.md to project directory
155
+ */
156
+ async function writePopeyeConfig(
157
+ projectDir: string,
158
+ config: PopeyeProjectConfig
159
+ ): Promise<void> {
160
+ const configPath = path.join(projectDir, 'popeye.md');
161
+
162
+ const content = `---
163
+ # Popeye Project Configuration
164
+ language: ${config.language}
165
+ reviewer: ${config.reviewer}
166
+ arbitrator: ${config.enableArbitration ? config.arbitrator : 'off'}
167
+ created: ${config.created}
168
+ lastRun: ${new Date().toISOString()}
169
+ ${config.projectName ? `projectName: ${config.projectName}` : ''}
170
+ ---
171
+
172
+ # ${config.projectName || 'Popeye Project'}
173
+
174
+ ${config.description ? `## Description\n${config.description}\n` : ''}
175
+ ## Notes
176
+ ${config.notes || 'Add any guidance or notes for Claude here...'}
177
+
178
+ ## Configuration
179
+ - **Language**: ${config.language}
180
+ - **Reviewer**: ${config.reviewer}
181
+ - **Arbitrator**: ${config.enableArbitration ? config.arbitrator : 'disabled'}
182
+
183
+ ## Session History
184
+ - ${config.created.split('T')[0]}: Project created
185
+ - ${new Date().toISOString().split('T')[0]}: Last session
186
+ `;
187
+
188
+ await fs.writeFile(configPath, content, 'utf-8');
189
+ }
190
+
191
+ /**
192
+ * Update lastRun in popeye.md without changing other content
193
+ */
194
+ async function updatePopeyeLastRun(projectDir: string): Promise<void> {
195
+ const configPath = path.join(projectDir, 'popeye.md');
196
+
197
+ try {
198
+ let content = await fs.readFile(configPath, 'utf-8');
199
+
200
+ // Update lastRun in frontmatter
201
+ content = content.replace(
202
+ /lastRun:\s*.+/,
203
+ `lastRun: ${new Date().toISOString()}`
204
+ );
205
+
206
+ await fs.writeFile(configPath, content, 'utf-8');
207
+ } catch {
208
+ // File doesn't exist, ignore
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Apply popeye.md config to session state
214
+ */
215
+ function applyPopeyeConfig(state: SessionState, config: PopeyeProjectConfig): void {
216
+ state.language = config.language;
217
+ state.reviewer = config.reviewer;
218
+ state.arbitrator = config.arbitrator;
219
+ state.enableArbitration = config.enableArbitration;
220
+ }
221
+
49
222
  // Note: startSpinner, succeedSpinner, failSpinner, stopSpinner are used in handleIdea
50
223
 
51
224
  /**
@@ -73,6 +246,7 @@ interface SessionState {
73
246
  claudeAuth: boolean;
74
247
  openaiAuth: boolean;
75
248
  geminiAuth: boolean;
249
+ grokAuth: boolean;
76
250
  reviewer: AIProvider;
77
251
  arbitrator: AIProvider;
78
252
  enableArbitration: boolean;
@@ -128,7 +302,7 @@ function drawInputBoxTop(state: SessionState): void {
128
302
 
129
303
  // Hints line (above the box)
130
304
  const hints = [
131
- theme.dim('/lang ') + theme.primary('py') + theme.dim('|') + theme.primary('ts'),
305
+ theme.dim('/lang ') + theme.primary('py') + theme.dim('|') + theme.primary('ts') + theme.dim('|') + theme.primary('fs'),
132
306
  theme.dim('/config'),
133
307
  theme.dim('/help'),
134
308
  theme.dim('/exit'),
@@ -136,10 +310,18 @@ function drawInputBoxTop(state: SessionState): void {
136
310
  console.log(' ' + hints.join(' '));
137
311
 
138
312
  // 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);
313
+ const langStatus = state.language === 'fullstack' ? 'fs' : state.language;
314
+ const reviewerStatus = state.reviewer === 'openai' ? 'O' : state.reviewer === 'grok' ? 'X' : 'G';
315
+ const arbitratorStatus = state.enableArbitration
316
+ ? (state.arbitrator === 'openai' ? 'O' : state.arbitrator === 'grok' ? 'X' : 'G')
317
+ : '-';
318
+ // Check auth based on which providers are configured
319
+ const reviewerAuthed = state.reviewer === 'openai' ? state.openaiAuth
320
+ : state.reviewer === 'grok' ? state.grokAuth : state.geminiAuth;
321
+ const arbitratorAuthed = !state.enableArbitration ? true
322
+ : state.arbitrator === 'openai' ? state.openaiAuth
323
+ : state.arbitrator === 'grok' ? state.grokAuth : state.geminiAuth;
324
+ const allAuth = state.claudeAuth && reviewerAuthed && arbitratorAuthed;
143
325
  const authIcon = allAuth ? '●' : '○';
144
326
  const authColor = allAuth ? theme.success : theme.warning;
145
327
 
@@ -300,6 +482,8 @@ async function ensureAuthentication(state: SessionState): Promise<boolean> {
300
482
  state.claudeAuth = status.claude.authenticated;
301
483
  state.openaiAuth = status.openai.authenticated;
302
484
  state.geminiAuth = status.gemini?.authenticated || false;
485
+ const grokStatus = await checkGrokAuth();
486
+ state.grokAuth = grokStatus.authenticated;
303
487
 
304
488
  console.log();
305
489
  printInfo('Checking authentication...');
@@ -362,6 +546,7 @@ async function ensureAuthentication(state: SessionState): Promise<boolean> {
362
546
  [
363
547
  { label: theme.secondary('OpenAI') + theme.dim(' - GPT-4o reviews plans'), value: 'openai' },
364
548
  { label: theme.secondary('Gemini') + theme.dim(' - Gemini 2.0 reviews plans'), value: 'gemini' },
549
+ { label: theme.secondary('Grok') + theme.dim(' - xAI Grok reviews plans'), value: 'grok' },
365
550
  ],
366
551
  'openai'
367
552
  ) as AIProvider;
@@ -374,14 +559,16 @@ async function ensureAuthentication(state: SessionState): Promise<boolean> {
374
559
  );
375
560
 
376
561
  if (state.enableArbitration) {
377
- // Auto-select the other provider as arbitrator
378
- const defaultArbitrator = state.reviewer === 'openai' ? 'gemini' : 'openai';
562
+ // Auto-select a different provider as arbitrator
563
+ const defaultArbitrator = state.reviewer === 'openai' ? 'gemini'
564
+ : state.reviewer === 'gemini' ? 'openai' : 'gemini';
379
565
 
380
566
  state.arbitrator = await promptSelection(
381
567
  'Who should arbitrate when stuck?',
382
568
  [
383
569
  { label: theme.secondary('Gemini') + theme.dim(' - Google Gemini breaks deadlocks'), value: 'gemini' },
384
570
  { label: theme.secondary('OpenAI') + theme.dim(' - OpenAI breaks deadlocks'), value: 'openai' },
571
+ { label: theme.secondary('Grok') + theme.dim(' - xAI Grok breaks deadlocks'), value: 'grok' },
385
572
  ],
386
573
  defaultArbitrator
387
574
  ) as AIProvider;
@@ -407,6 +594,39 @@ async function ensureAuthentication(state: SessionState): Promise<boolean> {
407
594
  state.enableArbitration = false;
408
595
  }
409
596
  }
597
+
598
+ // Authenticate Grok if needed for reviewer or arbitrator
599
+ const needsGrok = state.reviewer === 'grok' || state.arbitrator === 'grok';
600
+ if (needsGrok && !state.grokAuth) {
601
+ console.log();
602
+ console.log(theme.dim(box.vertical) + ' ' + theme.primary('Grok API') + theme.dim(' - Required for ' + (state.reviewer === 'grok' ? 'review' : 'arbitration')));
603
+ console.log(theme.dim(box.vertical));
604
+
605
+ try {
606
+ const success = await authenticateGrok();
607
+ if (success) {
608
+ printSuccess('Grok API ready');
609
+ state.grokAuth = true;
610
+ } else {
611
+ printWarning('Grok API not authenticated');
612
+ if (state.reviewer === 'grok') {
613
+ printWarning('Falling back to OpenAI as reviewer');
614
+ state.reviewer = 'openai';
615
+ }
616
+ if (state.arbitrator === 'grok') {
617
+ state.enableArbitration = false;
618
+ }
619
+ }
620
+ } catch (err) {
621
+ printError(err instanceof Error ? err.message : 'Authentication failed');
622
+ if (state.reviewer === 'grok') {
623
+ state.reviewer = 'openai';
624
+ }
625
+ if (state.arbitrator === 'grok') {
626
+ state.enableArbitration = false;
627
+ }
628
+ }
629
+ }
410
630
  }
411
631
 
412
632
  // Also check if reviewer is gemini and we need to auth
@@ -430,21 +650,46 @@ async function ensureAuthentication(state: SessionState): Promise<boolean> {
430
650
  }
431
651
  }
432
652
 
653
+ // Also check if reviewer is grok and we need to auth
654
+ if (state.reviewer === 'grok' && !state.grokAuth) {
655
+ console.log();
656
+ console.log(theme.dim(box.vertical) + ' ' + theme.primary('Grok API') + theme.dim(' - Required for review'));
657
+ console.log(theme.dim(box.vertical));
658
+
659
+ try {
660
+ const success = await authenticateGrok();
661
+ if (success) {
662
+ printSuccess('Grok API ready');
663
+ state.grokAuth = true;
664
+ } else {
665
+ printWarning('Grok API not authenticated - falling back to OpenAI');
666
+ state.reviewer = 'openai';
667
+ }
668
+ } catch (err) {
669
+ printError(err instanceof Error ? err.message : 'Authentication failed');
670
+ state.reviewer = 'openai';
671
+ }
672
+ }
673
+
433
674
  // Save the configuration to persist between sessions
434
675
  await saveConsensusConfig(state);
435
676
 
436
677
  // Show summary
437
678
  console.log();
438
679
  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')}`);
680
+ const reviewerName = state.reviewer === 'openai' ? 'OpenAI (GPT-4o)' : state.reviewer === 'grok' ? 'Grok' : 'Gemini';
681
+ const arbitratorName = state.arbitrator === 'openai' ? 'OpenAI' : state.arbitrator === 'grok' ? 'Grok' : 'Gemini';
682
+ console.log(` ${theme.dim('Reviewer:')} ${theme.primary(reviewerName)}`);
683
+ console.log(` ${theme.dim('Arbitrator:')} ${state.enableArbitration ? theme.primary(arbitratorName) : theme.dim('Disabled')}`);
441
684
  console.log();
442
685
  } else if (state.claudeAuth && state.openaiAuth && alreadyConfigured) {
443
686
  // Show loaded configuration
444
687
  console.log();
445
688
  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')}`);
689
+ const savedReviewerName = state.reviewer === 'openai' ? 'OpenAI (GPT-4o)' : state.reviewer === 'grok' ? 'Grok' : 'Gemini';
690
+ const savedArbitratorName = state.arbitrator === 'openai' ? 'OpenAI' : state.arbitrator === 'grok' ? 'Grok' : 'Gemini';
691
+ console.log(` ${theme.dim('Reviewer:')} ${theme.primary(savedReviewerName)}`);
692
+ console.log(` ${theme.dim('Arbitrator:')} ${state.enableArbitration ? theme.primary(savedArbitratorName) : theme.dim('Disabled')}`);
448
693
  console.log();
449
694
 
450
695
  // Authenticate Gemini if needed based on saved config
@@ -474,6 +719,34 @@ async function ensureAuthentication(state: SessionState): Promise<boolean> {
474
719
  }
475
720
  console.log();
476
721
  }
722
+
723
+ // Authenticate Grok if needed based on saved config
724
+ const needsGrok = state.reviewer === 'grok' || (state.enableArbitration && state.arbitrator === 'grok');
725
+ if (needsGrok && !state.grokAuth) {
726
+ console.log(theme.dim(box.vertical) + ' ' + theme.primary('Grok API') + theme.dim(' - Required for ' + (state.reviewer === 'grok' ? 'review' : 'arbitration')));
727
+ console.log(theme.dim(box.vertical));
728
+
729
+ try {
730
+ const success = await authenticateGrok();
731
+ if (success) {
732
+ printSuccess('Grok API ready');
733
+ state.grokAuth = true;
734
+ } else {
735
+ printWarning('Grok API not authenticated');
736
+ if (state.reviewer === 'grok') {
737
+ printWarning('Falling back to OpenAI as reviewer');
738
+ state.reviewer = 'openai';
739
+ }
740
+ if (state.enableArbitration && state.arbitrator === 'grok') {
741
+ printWarning('Disabling arbitration');
742
+ state.enableArbitration = false;
743
+ }
744
+ }
745
+ } catch (err) {
746
+ printError(err instanceof Error ? err.message : 'Grok authentication failed');
747
+ }
748
+ console.log();
749
+ }
477
750
  }
478
751
 
479
752
  return state.claudeAuth && state.openaiAuth;
@@ -493,9 +766,9 @@ function showHelp(): void {
493
766
  ['/status', 'Show current project status'],
494
767
  ['/auth', 'Re-authenticate services'],
495
768
  ['/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)'],
769
+ ['/config reviewer', 'Set reviewer (openai/gemini/grok)'],
770
+ ['/config arbitrator', 'Set arbitrator (openai/gemini/grok/off)'],
771
+ ['/lang <lang>', 'Set language (python/typescript/fullstack)'],
499
772
  ['/new <idea>', 'Force start a new project (skips existing check)'],
500
773
  ['/resume', 'Resume interrupted project'],
501
774
  ['/clear', 'Clear screen'],
@@ -557,6 +830,14 @@ async function handleInfo(): Promise<void> {
557
830
  console.log(` ${theme.dim('API Key:')} ${theme.dim(geminiStatus.keyLastFour)}`);
558
831
  }
559
832
 
833
+ console.log();
834
+ console.log(theme.secondary(' Grok:'));
835
+ const grokStatus = await checkGrokAuth();
836
+ console.log(` ${theme.dim('Authenticated:')} ${grokStatus.authenticated ? theme.success('Yes') : theme.dim('No')}`);
837
+ if (grokStatus.authenticated && grokStatus.keyLastFour) {
838
+ console.log(` ${theme.dim('API Key:')} ${theme.dim(grokStatus.keyLastFour)}`);
839
+ }
840
+
560
841
  console.log();
561
842
  console.log(theme.secondary(' Environment:'));
562
843
  console.log(` ${theme.dim('Node.js:')} ${process.version}`);
@@ -707,32 +988,40 @@ async function handleConfig(state: SessionState, args: string[] = []): Promise<v
707
988
  case 'reviewer':
708
989
  if (args.length > 1) {
709
990
  const newReviewer = args[1].toLowerCase();
710
- if (newReviewer === 'openai' || newReviewer === 'gemini') {
991
+ if (newReviewer === 'openai' || newReviewer === 'gemini' || newReviewer === 'grok') {
711
992
  if (newReviewer === 'gemini' && !state.geminiAuth) {
712
993
  printWarning('Gemini API not authenticated. Run /auth first.');
713
994
  return;
714
995
  }
996
+ if (newReviewer === 'grok' && !state.grokAuth) {
997
+ printWarning('Grok API not authenticated. Run /auth first.');
998
+ return;
999
+ }
715
1000
  state.reviewer = newReviewer as AIProvider;
716
1001
  // Save to config
717
1002
  await saveConsensusConfig(state);
718
1003
  printSuccess(`Reviewer set to ${newReviewer}`);
719
1004
  } else {
720
- printError('Invalid reviewer. Use: openai or gemini');
1005
+ printError('Invalid reviewer. Use: openai, gemini, or grok');
721
1006
  }
722
1007
  } else {
723
1008
  printKeyValue('Reviewer', state.reviewer);
724
- printInfo('Use: /config reviewer <openai|gemini>');
1009
+ printInfo('Use: /config reviewer <openai|gemini|grok>');
725
1010
  }
726
1011
  return;
727
1012
 
728
1013
  case 'arbitrator':
729
1014
  if (args.length > 1) {
730
1015
  const newArbitrator = args[1].toLowerCase();
731
- if (newArbitrator === 'openai' || newArbitrator === 'gemini') {
1016
+ if (newArbitrator === 'openai' || newArbitrator === 'gemini' || newArbitrator === 'grok') {
732
1017
  if (newArbitrator === 'gemini' && !state.geminiAuth) {
733
1018
  printWarning('Gemini API not authenticated. Run /auth first.');
734
1019
  return;
735
1020
  }
1021
+ if (newArbitrator === 'grok' && !state.grokAuth) {
1022
+ printWarning('Grok API not authenticated. Run /auth first.');
1023
+ return;
1024
+ }
736
1025
  state.arbitrator = newArbitrator as AIProvider;
737
1026
  state.enableArbitration = true;
738
1027
  // Save to config
@@ -744,23 +1033,33 @@ async function handleConfig(state: SessionState, args: string[] = []): Promise<v
744
1033
  await saveConsensusConfig(state);
745
1034
  printSuccess('Arbitration disabled');
746
1035
  } else {
747
- printError('Invalid arbitrator. Use: openai, gemini, or off');
1036
+ printError('Invalid arbitrator. Use: openai, gemini, grok, or off');
748
1037
  }
749
1038
  } else {
750
1039
  printKeyValue('Arbitrator', state.enableArbitration ? state.arbitrator : 'disabled');
751
- printInfo('Use: /config arbitrator <openai|gemini|off>');
1040
+ printInfo('Use: /config arbitrator <openai|gemini|grok|off>');
752
1041
  }
753
1042
  return;
754
1043
 
755
1044
  case 'language':
756
1045
  case 'lang':
757
1046
  if (args.length > 1) {
758
- const lang = args[1].toLowerCase() as OutputLanguage;
759
- if (['python', 'typescript'].includes(lang)) {
1047
+ // Map shortcuts to full language names
1048
+ const langAliases: Record<string, OutputLanguage> = {
1049
+ 'py': 'python',
1050
+ 'python': 'python',
1051
+ 'ts': 'typescript',
1052
+ 'typescript': 'typescript',
1053
+ 'fs': 'fullstack',
1054
+ 'fullstack': 'fullstack',
1055
+ };
1056
+ const input = args[1].toLowerCase();
1057
+ const lang = langAliases[input];
1058
+ if (lang) {
760
1059
  state.language = lang;
761
1060
  printSuccess(`Language set to ${lang}`);
762
1061
  } else {
763
- printError('Invalid language. Use: python or typescript');
1062
+ printError('Invalid language. Use: py, ts, fs (or python, typescript, fullstack)');
764
1063
  }
765
1064
  } else {
766
1065
  printKeyValue('Language', state.language);
@@ -784,19 +1083,22 @@ async function handleConfig(state: SessionState, args: string[] = []): Promise<v
784
1083
  console.log(` ${theme.dim('Claude:')} ${state.claudeAuth ? theme.success('● Ready') : theme.error('○ Not authenticated')}`);
785
1084
  console.log(` ${theme.dim('OpenAI:')} ${state.openaiAuth ? theme.success('● Ready') : theme.error('○ Not authenticated')}`);
786
1085
  console.log(` ${theme.dim('Gemini:')} ${state.geminiAuth ? theme.success('● Ready') : theme.dim('○ Not configured')}`);
1086
+ console.log(` ${theme.dim('Grok:')} ${state.grokAuth ? theme.success('● Ready') : theme.dim('○ Not configured')}`);
787
1087
  console.log();
788
1088
  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')}`);
1089
+ const configReviewerName = state.reviewer === 'openai' ? 'OpenAI (GPT-4o)' : state.reviewer === 'grok' ? 'Grok' : 'Gemini';
1090
+ const configArbitratorName = state.arbitrator === 'openai' ? 'OpenAI' : state.arbitrator === 'grok' ? 'Grok' : 'Gemini';
1091
+ console.log(` ${theme.dim('Reviewer:')} ${theme.primary(configReviewerName)}`);
1092
+ console.log(` ${theme.dim('Arbitrator:')} ${state.enableArbitration ? theme.primary(configArbitratorName) : theme.dim('Disabled')}`);
791
1093
  console.log();
792
1094
  console.log(theme.primary.bold(' Consensus:'));
793
1095
  console.log(` ${theme.dim('Threshold:')} ${config.consensus.threshold}%`);
794
1096
  console.log(` ${theme.dim('Max Iters:')} ${config.consensus.max_disagreements}`);
795
1097
  console.log();
796
1098
  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>'));
1099
+ console.log(theme.dim(' /config reviewer <openai|gemini|grok>'));
1100
+ console.log(theme.dim(' /config arbitrator <openai|gemini|grok|off>'));
1101
+ console.log(theme.dim(' /config language <python|typescript|fullstack>'));
800
1102
  console.log();
801
1103
  }
802
1104
 
@@ -807,13 +1109,25 @@ function handleLanguage(args: string[], state: SessionState): void {
807
1109
  if (args.length === 0) {
808
1110
  console.log();
809
1111
  printKeyValue('Current language', state.language);
810
- printInfo('Use /language <python|typescript> to change');
1112
+ printInfo('Use /language <py|ts|fs> or <python|typescript|fullstack> to change');
811
1113
  return;
812
1114
  }
813
1115
 
814
- const lang = args[0].toLowerCase() as OutputLanguage;
815
- if (!['python', 'typescript'].includes(lang)) {
816
- printError('Invalid language. Use: python or typescript');
1116
+ // Map shortcuts to full language names
1117
+ const langAliases: Record<string, OutputLanguage> = {
1118
+ 'py': 'python',
1119
+ 'python': 'python',
1120
+ 'ts': 'typescript',
1121
+ 'typescript': 'typescript',
1122
+ 'fs': 'fullstack',
1123
+ 'fullstack': 'fullstack',
1124
+ };
1125
+
1126
+ const input = args[0].toLowerCase();
1127
+ const lang = langAliases[input];
1128
+
1129
+ if (!lang) {
1130
+ printError('Invalid language. Use: py, ts, fs (or python, typescript, fullstack)');
817
1131
  return;
818
1132
  }
819
1133
 
@@ -1092,6 +1406,14 @@ async function handleResume(state: SessionState, args: string[]): Promise<void>
1092
1406
  return;
1093
1407
  }
1094
1408
 
1409
+ // Check for popeye.md and load project-specific configuration
1410
+ const popeyeConfig = await readPopeyeConfig(state.projectDir);
1411
+ if (popeyeConfig) {
1412
+ applyPopeyeConfig(state, popeyeConfig);
1413
+ await updatePopeyeLastRun(state.projectDir);
1414
+ printInfo(`Loaded config from popeye.md (${popeyeConfig.language}, reviewer: ${popeyeConfig.reviewer})`);
1415
+ }
1416
+
1095
1417
  const status = await getWorkflowStatus(state.projectDir);
1096
1418
 
1097
1419
  if (status.exists && status.state) {
@@ -1448,7 +1770,45 @@ async function handleResume(state: SessionState, args: string[]): Promise<void>
1448
1770
  * Extracts key nouns and creates a kebab-case name
1449
1771
  */
1450
1772
  function generateProjectName(idea: string): string {
1451
- // Common words to filter out
1773
+ // 1. First, try to find explicit project name patterns
1774
+ const explicitPatterns = [
1775
+ /(?:called|named|for|planning|project)\s+["']?([A-Z][a-zA-Z0-9]+)["']?/i,
1776
+ /["']([A-Z][a-zA-Z0-9]+)["']/, // Quoted names
1777
+ /\b([A-Z][a-z]+(?:[A-Z][a-z]+)+)\b/, // CamelCase names like "TodoMaster"
1778
+ ];
1779
+
1780
+ for (const pattern of explicitPatterns) {
1781
+ const match = idea.match(pattern);
1782
+ if (match && match[1] && match[1].length >= 3 && match[1].length <= 30) {
1783
+ // Convert to kebab-case
1784
+ return match[1]
1785
+ .replace(/([a-z])([A-Z])/g, '$1-$2')
1786
+ .toLowerCase()
1787
+ .replace(/[^a-z0-9-]/g, '-')
1788
+ .replace(/-+/g, '-')
1789
+ .replace(/^-|-$/g, '');
1790
+ }
1791
+ }
1792
+
1793
+ // 2. Look for standalone capitalized words (potential project names)
1794
+ // Exclude common capitalized words at sentence start
1795
+ const capitalizedWords = idea.match(/\b([A-Z][a-z]{2,})\b/g) || [];
1796
+ const excludeCapitalized = new Set([
1797
+ 'Build', 'Create', 'Make', 'Develop', 'Write', 'Implement', 'Design',
1798
+ 'Read', 'Start', 'Help', 'Please', 'Want', 'Need', 'Use', 'Add',
1799
+ 'The', 'This', 'That', 'What', 'When', 'Where', 'How', 'Why',
1800
+ ]);
1801
+
1802
+ const projectNameCandidates = capitalizedWords.filter(
1803
+ w => !excludeCapitalized.has(w) && w.length >= 3
1804
+ );
1805
+
1806
+ if (projectNameCandidates.length > 0) {
1807
+ // Use the first non-excluded capitalized word
1808
+ return projectNameCandidates[0].toLowerCase();
1809
+ }
1810
+
1811
+ // 3. Fall back to extracting meaningful words
1452
1812
  const stopWords = new Set([
1453
1813
  'a', 'an', 'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
1454
1814
  'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'were', 'been',
@@ -1458,6 +1818,9 @@ function generateProjectName(idea: string): string {
1458
1818
  'want', 'like', 'please', 'help', 'me', 'i', 'my', 'we', 'our', 'you',
1459
1819
  'your', 'that', 'which', 'who', 'what', 'where', 'when', 'why', 'how',
1460
1820
  'this', 'these', 'those', 'it', 'its', 'simple', 'basic', 'new',
1821
+ // Action verbs that shouldn't be project names
1822
+ 'read', 'start', 'planning', 'reading', 'starting', 'begin', 'beginning',
1823
+ 'all', 'files', 'file', 'directory', 'folder', 'also',
1461
1824
  ]);
1462
1825
 
1463
1826
  // Extract meaningful words
@@ -1638,6 +2001,19 @@ async function handleIdea(idea: string, state: SessionState): Promise<void> {
1638
2001
 
1639
2002
  succeedSpinner(`Created ${scaffoldResult.filesCreated.length} files`);
1640
2003
 
2004
+ // Create popeye.md with project configuration
2005
+ await writePopeyeConfig(projectDir, {
2006
+ language: state.language,
2007
+ reviewer: state.reviewer,
2008
+ arbitrator: state.arbitrator,
2009
+ enableArbitration: state.enableArbitration,
2010
+ created: new Date().toISOString(),
2011
+ lastRun: new Date().toISOString(),
2012
+ projectName,
2013
+ description: idea,
2014
+ });
2015
+ printInfo('Created popeye.md with project configuration');
2016
+
1641
2017
  // Run workflow with reviewer/arbitrator settings
1642
2018
  console.log();
1643
2019
  printInfo('Starting AI workflow...');
@@ -1726,6 +2102,19 @@ async function handleNewProject(idea: string, state: SessionState): Promise<void
1726
2102
 
1727
2103
  succeedSpinner(`Created ${scaffoldResult.filesCreated.length} files`);
1728
2104
 
2105
+ // Create popeye.md with project configuration
2106
+ await writePopeyeConfig(projectDir, {
2107
+ language: state.language,
2108
+ reviewer: state.reviewer,
2109
+ arbitrator: state.arbitrator,
2110
+ enableArbitration: state.enableArbitration,
2111
+ created: new Date().toISOString(),
2112
+ lastRun: new Date().toISOString(),
2113
+ projectName,
2114
+ description: idea,
2115
+ });
2116
+ printInfo('Created popeye.md with project configuration');
2117
+
1729
2118
  // Run workflow with reviewer/arbitrator settings
1730
2119
  console.log();
1731
2120
  printInfo('Starting AI workflow...');
@@ -1779,6 +2168,7 @@ export async function startInteractiveMode(): Promise<void> {
1779
2168
  claudeAuth: false,
1780
2169
  openaiAuth: false,
1781
2170
  geminiAuth: false,
2171
+ grokAuth: false,
1782
2172
  // Load saved reviewer/arbitrator settings from config
1783
2173
  reviewer: config.consensus.reviewer,
1784
2174
  arbitrator: config.consensus.arbitrator === 'off' ? 'openai' : config.consensus.arbitrator,
@@ -1795,7 +2185,7 @@ export async function startInteractiveMode(): Promise<void> {
1795
2185
  console.log(theme.dim(' ├─ ') + theme.secondary('Reviewer (configurable)') + theme.dim(' - Reviews plans until consensus'));
1796
2186
  console.log(theme.dim(' └─ ') + theme.secondary('Arbitrator (optional)') + theme.dim(' - Breaks deadlocks when stuck'));
1797
2187
  console.log();
1798
- console.log(theme.dim(' You can choose OpenAI or Gemini as reviewer/arbitrator during setup.'));
2188
+ console.log(theme.dim(' You can choose OpenAI, Gemini, or Grok as reviewer/arbitrator during setup.'));
1799
2189
  console.log(theme.dim(' Plans are saved to docs/ folder in markdown format.'));
1800
2190
  console.log();
1801
2191