polydev-ai 1.8.16 → 1.8.18

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 (2) hide show
  1. package/mcp/stdio-wrapper.js +137 -42
  2. package/package.json +1 -1
@@ -103,6 +103,12 @@ if (writableTmp) {
103
103
  /**
104
104
  * Clean CLI response by removing metadata and debug info
105
105
  * Strips: provider info, approval, sandbox, reasoning, session id, MCP errors, etc.
106
+ *
107
+ * Codex CLI output structure:
108
+ * [metadata] → user → [echoed prompt] → thinking → [status] → codex → [RESPONSE]
109
+ *
110
+ * Claude Code output structure:
111
+ * [may include JSON or plain text response]
106
112
  */
107
113
  function cleanCliResponse(content) {
108
114
  if (!content || typeof content !== 'string') {
@@ -111,57 +117,108 @@ function cleanCliResponse(content) {
111
117
 
112
118
  const lines = content.split('\n');
113
119
  const cleanedLines = [];
114
- let skipUntilContent = true;
115
- let inThinkingSection = false;
116
120
 
117
- for (const line of lines) {
121
+ // State machine for Codex CLI output parsing
122
+ // States: 'metadata' | 'user_section' | 'thinking_section' | 'response'
123
+ let state = 'metadata';
124
+ let foundCodexMarker = false;
125
+
126
+ for (let i = 0; i < lines.length; i++) {
127
+ const line = lines[i];
118
128
  const trimmed = line.trim();
119
129
 
120
- // Skip metadata lines at the start
121
- if (skipUntilContent) {
122
- // Skip empty lines
123
- if (!trimmed) continue;
124
-
130
+ // Always skip MCP errors anywhere
131
+ if (trimmed.startsWith('ERROR: MCP client for')) continue;
132
+ if (trimmed.includes('failed to start') && trimmed.includes('MCP')) continue;
133
+ if (trimmed.includes('handshake') && trimmed.includes('failed')) continue;
134
+ if (trimmed.includes('connection closed')) continue;
135
+
136
+ // State: metadata - skip until we hit a section marker
137
+ if (state === 'metadata') {
125
138
  // Skip known metadata patterns
139
+ if (!trimmed) continue;
126
140
  if (trimmed.startsWith('provider:')) continue;
127
141
  if (trimmed.startsWith('approval:')) continue;
128
142
  if (trimmed.startsWith('sandbox:')) continue;
129
143
  if (trimmed.startsWith('reasoning effort:')) continue;
130
144
  if (trimmed.startsWith('reasoning summaries:')) continue;
131
145
  if (trimmed.startsWith('session id:')) continue;
132
- if (trimmed.match(/^-{4,}$/)) continue; // Skip separator lines like --------
133
- if (trimmed === 'user') continue; // Skip role markers
134
- if (trimmed === 'assistant') continue;
146
+ if (trimmed.match(/^-{4,}$/)) continue; // Separator lines
147
+
148
+ // Transition to user section
149
+ if (trimmed === 'user') {
150
+ state = 'user_section';
151
+ continue;
152
+ }
135
153
 
136
- // Skip MCP client errors
137
- if (trimmed.startsWith('ERROR: MCP client for')) continue;
138
- if (trimmed.includes('failed to start')) continue;
154
+ // If we hit 'codex' directly (no user section), go to response
155
+ if (trimmed === 'codex') {
156
+ state = 'response';
157
+ foundCodexMarker = true;
158
+ continue;
159
+ }
160
+
161
+ // If we hit 'assistant' (Claude Code), this might be actual content
162
+ if (trimmed === 'assistant') {
163
+ state = 'response';
164
+ continue;
165
+ }
166
+
167
+ // If no Codex markers found and we have content, it might be Claude Code output
168
+ // Check if it looks like actual content (not metadata)
169
+ if (!trimmed.includes(':') || trimmed.length > 100) {
170
+ state = 'response';
171
+ cleanedLines.push(line);
172
+ continue;
173
+ }
139
174
 
140
- // Found actual content - stop skipping
141
- skipUntilContent = false;
175
+ continue;
142
176
  }
143
177
 
144
- // Handle thinking sections (optional: can show or hide)
145
- if (trimmed === 'thinking') {
146
- inThinkingSection = true;
147
- continue; // Skip the "thinking" marker
178
+ // State: user_section - skip echoed prompt until thinking/codex
179
+ if (state === 'user_section') {
180
+ if (trimmed === 'thinking') {
181
+ state = 'thinking_section';
182
+ continue;
183
+ }
184
+ if (trimmed === 'codex') {
185
+ state = 'response';
186
+ foundCodexMarker = true;
187
+ continue;
188
+ }
189
+ // Skip everything in user section (echoed prompt, errors)
190
+ continue;
148
191
  }
149
192
 
150
- // End of thinking section on empty line after content
151
- if (inThinkingSection && !trimmed) {
152
- inThinkingSection = false;
193
+ // State: thinking_section - skip until codex marker
194
+ if (state === 'thinking_section') {
195
+ if (trimmed === 'codex') {
196
+ state = 'response';
197
+ foundCodexMarker = true;
198
+ continue;
199
+ }
200
+ // Skip thinking content (bold status messages, reasoning)
153
201
  continue;
154
202
  }
155
203
 
156
- // Skip more MCP errors anywhere in output
157
- if (trimmed.startsWith('ERROR: MCP client for')) continue;
158
- if (trimmed.includes('failed to start') && trimmed.includes('MCP')) continue;
159
-
160
- // Keep this line
161
- cleanedLines.push(line);
204
+ // State: response - keep actual response content
205
+ if (state === 'response') {
206
+ // Skip footer patterns
207
+ if (trimmed === 'tokens used') continue;
208
+ if (trimmed.match(/^[\d,]+$/) && i === lines.length - 1) continue; // Token count at end
209
+
210
+ // Skip any remaining bold status markers
211
+ if (trimmed.match(/^\*\*.*\*\*$/)) continue;
212
+
213
+ // Keep this line
214
+ cleanedLines.push(line);
215
+ }
162
216
  }
163
217
 
164
- // Trim trailing empty lines and return
218
+ // Trim leading/trailing empty lines
219
+ while (cleanedLines.length > 0 && !cleanedLines[0].trim()) {
220
+ cleanedLines.shift();
221
+ }
165
222
  while (cleanedLines.length > 0 && !cleanedLines[cleanedLines.length - 1].trim()) {
166
223
  cleanedLines.pop();
167
224
  }
@@ -188,6 +245,7 @@ class StdioMCPWrapper {
188
245
 
189
246
  // Cache for user model preferences (provider -> model)
190
247
  this.userModelPreferences = null;
248
+ this.perspectivesPerMessage = 2; // Default to 2 perspectives
191
249
  this.modelPreferencesCacheTime = null;
192
250
  this.MODEL_PREFERENCES_CACHE_TTL = 5 * 60 * 1000; // 5 minutes cache
193
251
  }
@@ -507,6 +565,7 @@ class StdioMCPWrapper {
507
565
 
508
566
  /**
509
567
  * Local CLI prompt sending with ALL available CLIs + remote perspectives
568
+ * Respects user's perspectives_per_message setting for total perspectives
510
569
  */
511
570
  async localSendCliPrompt(args) {
512
571
  console.error(`[Stdio Wrapper] Local CLI prompt sending with perspectives`);
@@ -528,6 +587,7 @@ class StdioMCPWrapper {
528
587
  const gracefulTimeout = Math.min(timeout_ms, 600000);
529
588
 
530
589
  // Fetch user's model preferences (cached, non-blocking on failure)
590
+ // This also fetches perspectivesPerMessage setting
531
591
  let modelPreferences = {};
532
592
  try {
533
593
  modelPreferences = await this.fetchUserModelPreferences();
@@ -535,6 +595,10 @@ class StdioMCPWrapper {
535
595
  console.error('[Stdio Wrapper] Model preferences fetch failed (will use CLI defaults):', prefError.message);
536
596
  }
537
597
 
598
+ // Get the user's perspectives_per_message setting (default 2)
599
+ const maxPerspectives = this.perspectivesPerMessage || 2;
600
+ console.error(`[Stdio Wrapper] Max perspectives per message: ${maxPerspectives}`);
601
+
538
602
  let localResults = [];
539
603
 
540
604
  if (provider_id) {
@@ -549,15 +613,18 @@ class StdioMCPWrapper {
549
613
  const result = await this.cliManager.sendCliPrompt(provider_id, prompt, mode, gracefulTimeout, model);
550
614
  localResults = [{ provider_id, ...result }];
551
615
  } else {
552
- // No specific provider - use ALL available local CLIs
553
- console.error(`[Stdio Wrapper] Using all available local CLIs`);
554
- const availableProviders = await this.getAllAvailableProviders();
616
+ // No specific provider - use available local CLIs up to maxPerspectives limit
617
+ console.error(`[Stdio Wrapper] Using available local CLIs (max: ${maxPerspectives})`);
618
+ const allAvailableProviders = await this.getAllAvailableProviders();
619
+
620
+ // Limit to maxPerspectives
621
+ const availableProviders = allAvailableProviders.slice(0, maxPerspectives);
555
622
 
556
623
  if (availableProviders.length === 0) {
557
624
  console.error(`[Stdio Wrapper] No local CLIs available, will use remote perspectives only`);
558
625
  localResults = [];
559
626
  } else {
560
- console.error(`[Stdio Wrapper] Found ${availableProviders.length} available CLIs: ${availableProviders.join(', ')}`);
627
+ console.error(`[Stdio Wrapper] Found ${allAvailableProviders.length} available CLIs, using ${availableProviders.length}: ${availableProviders.join(', ')}`);
561
628
 
562
629
  // Run all CLI prompts concurrently
563
630
  const cliPromises = availableProviders.map(async (providerId) => {
@@ -588,8 +655,25 @@ class StdioMCPWrapper {
588
655
  console.error('[Stdio Wrapper] CLI results reporting failed (non-critical):', err.message);
589
656
  });
590
657
 
591
- // Get remote perspectives (only for models not covered by local CLIs)
592
- const perspectivesResult = await this.callPerspectivesForCli(args, localResults);
658
+ // Calculate how many successful local perspectives we got
659
+ const successfulLocalCount = localResults.filter(r => r.success).length;
660
+ const remainingPerspectives = maxPerspectives - successfulLocalCount;
661
+
662
+ // Get remote perspectives only if we need more perspectives to reach maxPerspectives
663
+ let perspectivesResult;
664
+ if (remainingPerspectives > 0) {
665
+ console.error(`[Stdio Wrapper] Need ${remainingPerspectives} more perspectives from remote`);
666
+ perspectivesResult = await this.callPerspectivesForCli(args, localResults, remainingPerspectives);
667
+ } else {
668
+ console.error(`[Stdio Wrapper] Already have ${successfulLocalCount} perspectives, skipping remote call`);
669
+ perspectivesResult = {
670
+ success: true,
671
+ content: '',
672
+ skipped: true,
673
+ reason: `Already have ${successfulLocalCount} perspectives (max: ${maxPerspectives})`,
674
+ timestamp: new Date().toISOString()
675
+ };
676
+ }
593
677
 
594
678
  // Record usage for all CLI responses
595
679
  for (const localResult of localResults) {
@@ -725,8 +809,11 @@ class StdioMCPWrapper {
725
809
  /**
726
810
  * Call remote perspectives for CLI prompts
727
811
  * Only calls remote APIs for providers NOT covered by successful local CLIs
812
+ * @param {Object} args - Original request arguments
813
+ * @param {Array} localResults - Results from local CLIs
814
+ * @param {number} maxPerspectives - Maximum number of remote perspectives to fetch
728
815
  */
729
- async callPerspectivesForCli(args, localResults) {
816
+ async callPerspectivesForCli(args, localResults, maxPerspectives = 2) {
730
817
  // Determine which providers succeeded locally
731
818
  const successfulLocalProviders = localResults
732
819
  .filter(r => r.success)
@@ -743,20 +830,21 @@ class StdioMCPWrapper {
743
830
  .map(cli => cliToApiProvider[cli])
744
831
  .filter(Boolean);
745
832
 
746
- // If all major providers are covered locally, skip remote call entirely
747
- if (excludeProviders.length >= 3 ||
833
+ // If all major providers are covered locally OR we don't need more perspectives, skip remote call
834
+ if (maxPerspectives <= 0 ||
835
+ excludeProviders.length >= 3 ||
748
836
  (excludeProviders.includes('anthropic') && excludeProviders.includes('openai') && excludeProviders.includes('google'))) {
749
- console.error(`[Stdio Wrapper] All providers covered by local CLIs, skipping remote perspectives`);
837
+ console.error(`[Stdio Wrapper] All providers covered by local CLIs or max perspectives reached, skipping remote perspectives`);
750
838
  return {
751
839
  success: true,
752
840
  content: '',
753
841
  skipped: true,
754
- reason: 'All providers covered by local CLIs',
842
+ reason: 'All providers covered by local CLIs or max perspectives reached',
755
843
  timestamp: new Date().toISOString()
756
844
  };
757
845
  }
758
846
 
759
- console.error(`[Stdio Wrapper] Calling remote perspectives (excluding: ${excludeProviders.join(', ') || 'none'})`);
847
+ console.error(`[Stdio Wrapper] Calling remote perspectives (excluding: ${excludeProviders.join(', ') || 'none'}, max: ${maxPerspectives})`);
760
848
 
761
849
  // Format CLI responses for logging on the server
762
850
  const cliResponses = localResults.map(result => ({
@@ -782,6 +870,8 @@ class StdioMCPWrapper {
782
870
  exclude_providers: excludeProviders,
783
871
  // Pass CLI responses for dashboard logging
784
872
  cli_responses: cliResponses,
873
+ // Limit remote perspectives to what we need
874
+ max_perspectives: maxPerspectives,
785
875
  project_memory: 'none',
786
876
  temperature: 0.7,
787
877
  max_tokens: 20000
@@ -1250,6 +1340,7 @@ class StdioMCPWrapper {
1250
1340
  /**
1251
1341
  * Fetch user's model preferences from API keys
1252
1342
  * Returns a map of CLI provider -> default_model
1343
+ * Also fetches and caches perspectivesPerMessage setting
1253
1344
  */
1254
1345
  async fetchUserModelPreferences() {
1255
1346
  // Check cache first
@@ -1285,7 +1376,11 @@ class StdioMCPWrapper {
1285
1376
  this.userModelPreferences = result.modelPreferences;
1286
1377
  this.modelPreferencesCacheTime = Date.now();
1287
1378
 
1379
+ // Also cache perspectives_per_message setting (default 2)
1380
+ this.perspectivesPerMessage = result.perspectivesPerMessage || 2;
1381
+
1288
1382
  console.error('[Stdio Wrapper] Model preferences loaded:', JSON.stringify(result.modelPreferences));
1383
+ console.error('[Stdio Wrapper] Perspectives per message:', this.perspectivesPerMessage);
1289
1384
  return result.modelPreferences;
1290
1385
  } else {
1291
1386
  console.error('[Stdio Wrapper] No model preferences in response');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polydev-ai",
3
- "version": "1.8.16",
3
+ "version": "1.8.18",
4
4
  "description": "Agentic workflow assistant with CLI integration - get diverse perspectives from multiple LLMs when stuck or need enhanced reasoning",
5
5
  "keywords": [
6
6
  "mcp",