@in-the-loop-labs/pair-review 1.3.1 → 1.3.2
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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/js/components/AnalysisConfigModal.js +3 -3
- package/public/js/local.js +2 -2
- package/public/js/modules/analysis-history.js +4 -0
- package/public/js/pr.js +2 -2
- package/public/js/repo-settings.js +4 -16
- package/src/ai/analyzer.js +2 -2
- package/src/ai/claude-cli.js +6 -1
- package/src/ai/claude-provider.js +153 -45
- package/src/ai/codex-provider.js +5 -0
- package/src/ai/cursor-agent-provider.js +5 -0
- package/src/ai/gemini-provider.js +5 -0
- package/src/ai/opencode-provider.js +5 -0
- package/src/ai/provider.js +7 -1
- package/src/config.js +1 -1
- package/src/main.js +3 -2
- package/src/routes/mcp.js +2 -2
- package/src/routes/shared.js +2 -2
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pair-review",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "pair-review app integration — Open PRs and local changes in the pair-review web UI, run server-side AI analysis, and address review feedback. Requires the pair-review MCP server.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "in-the-loop-labs",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-critic",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "AI-powered code review analysis — Run three-level AI analysis and implement-review-fix loops directly in your coding agent. Works standalone, no server required.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "in-the-loop-labs",
|
|
@@ -11,7 +11,7 @@ class AnalysisConfigModal {
|
|
|
11
11
|
this.onCancel = null;
|
|
12
12
|
this.escapeHandler = null;
|
|
13
13
|
this.selectedProvider = 'claude';
|
|
14
|
-
this.selectedModel = '
|
|
14
|
+
this.selectedModel = 'opus';
|
|
15
15
|
this.selectedPresets = new Set();
|
|
16
16
|
this.rememberModel = false;
|
|
17
17
|
this.repoInstructions = '';
|
|
@@ -92,9 +92,9 @@ class AnalysisConfigModal {
|
|
|
92
92
|
id: 'claude',
|
|
93
93
|
name: 'Claude',
|
|
94
94
|
models: [
|
|
95
|
-
{ id: '
|
|
95
|
+
{ id: 'opus', name: 'Opus 4.6 High', tier: 'thorough', default: true }
|
|
96
96
|
],
|
|
97
|
-
defaultModel: '
|
|
97
|
+
defaultModel: 'opus'
|
|
98
98
|
}
|
|
99
99
|
};
|
|
100
100
|
this.models = this.providers.claude.models;
|
package/public/js/local.js
CHANGED
|
@@ -363,7 +363,7 @@ class LocalManager {
|
|
|
363
363
|
const providerStorageKey = `pair-review-provider:local-${reviewId}`;
|
|
364
364
|
const rememberedModel = localStorage.getItem(modelStorageKey);
|
|
365
365
|
const rememberedProvider = localStorage.getItem(providerStorageKey);
|
|
366
|
-
const currentModel = rememberedModel || repoSettings?.default_model || '
|
|
366
|
+
const currentModel = rememberedModel || repoSettings?.default_model || 'opus';
|
|
367
367
|
const currentProvider = rememberedProvider || repoSettings?.default_provider || 'claude';
|
|
368
368
|
|
|
369
369
|
// Show config modal
|
|
@@ -1027,7 +1027,7 @@ class LocalManager {
|
|
|
1027
1027
|
},
|
|
1028
1028
|
body: JSON.stringify({
|
|
1029
1029
|
provider: config.provider || 'claude',
|
|
1030
|
-
model: config.model || '
|
|
1030
|
+
model: config.model || 'opus',
|
|
1031
1031
|
tier: config.tier || 'balanced',
|
|
1032
1032
|
customInstructions: config.customInstructions || null,
|
|
1033
1033
|
skipLevel3: config.skipLevel3 || false
|
|
@@ -734,6 +734,10 @@ class AnalysisHistoryManager {
|
|
|
734
734
|
'haiku': 'fast',
|
|
735
735
|
'sonnet': 'balanced',
|
|
736
736
|
'opus': 'thorough',
|
|
737
|
+
'opus-4.5': 'balanced',
|
|
738
|
+
'opus-4.6-low': 'balanced',
|
|
739
|
+
'opus-4.6-medium': 'balanced',
|
|
740
|
+
'opus-4.6-1m': 'balanced',
|
|
737
741
|
// Gemini models
|
|
738
742
|
'flash': 'fast',
|
|
739
743
|
'pro': 'balanced',
|
package/public/js/pr.js
CHANGED
|
@@ -3515,7 +3515,7 @@ class PRManager {
|
|
|
3515
3515
|
const providerStorageKey = PRManager.getRepoStorageKey('pair-review-provider', owner, repo);
|
|
3516
3516
|
const rememberedModel = localStorage.getItem(modelStorageKey);
|
|
3517
3517
|
const rememberedProvider = localStorage.getItem(providerStorageKey);
|
|
3518
|
-
const currentModel = rememberedModel || repoSettings?.default_model || '
|
|
3518
|
+
const currentModel = rememberedModel || repoSettings?.default_model || 'opus';
|
|
3519
3519
|
const currentProvider = rememberedProvider || repoSettings?.default_provider || 'claude';
|
|
3520
3520
|
|
|
3521
3521
|
// Show the config modal
|
|
@@ -3593,7 +3593,7 @@ class PRManager {
|
|
|
3593
3593
|
},
|
|
3594
3594
|
body: JSON.stringify({
|
|
3595
3595
|
provider: config.provider || 'claude',
|
|
3596
|
-
model: config.model || '
|
|
3596
|
+
model: config.model || 'opus',
|
|
3597
3597
|
tier: config.tier || 'balanced',
|
|
3598
3598
|
customInstructions: config.customInstructions || null,
|
|
3599
3599
|
skipLevel3: config.skipLevel3 || false
|
|
@@ -208,23 +208,11 @@ class RepoSettingsPage {
|
|
|
208
208
|
|
|
209
209
|
} catch (error) {
|
|
210
210
|
console.error('Error loading providers:', error);
|
|
211
|
-
//
|
|
212
|
-
// endpoint is unavailable
|
|
213
|
-
|
|
214
|
-
// src/ai/claude-provider.js - this fallback should mirror those values.
|
|
215
|
-
this.providers = {
|
|
216
|
-
claude: {
|
|
217
|
-
id: 'claude',
|
|
218
|
-
name: 'Claude',
|
|
219
|
-
models: [
|
|
220
|
-
{ id: 'haiku', name: 'Haiku', tier: 'fast', badge: 'Fastest', badgeClass: 'badge-speed', tagline: 'Lightning Fast', description: 'Quick analysis for simple changes' },
|
|
221
|
-
{ id: 'sonnet', name: 'Sonnet', tier: 'balanced', default: true, badge: 'Recommended', badgeClass: 'badge-recommended', tagline: 'Best Balance', description: 'Recommended for most reviews' },
|
|
222
|
-
{ id: 'opus', name: 'Opus', tier: 'thorough', badge: 'Most Thorough', badgeClass: 'badge-power', tagline: 'Most Capable', description: 'Deep analysis for complex code' }
|
|
223
|
-
],
|
|
224
|
-
defaultModel: 'sonnet'
|
|
225
|
-
}
|
|
226
|
-
};
|
|
211
|
+
// No hardcoded fallback — rely on the /api/providers endpoint as the single source of truth.
|
|
212
|
+
// If the endpoint is unavailable, show an empty state rather than stale data.
|
|
213
|
+
this.providers = {};
|
|
227
214
|
this.renderProviderButtons();
|
|
215
|
+
this.showToast('error', 'Failed to load AI providers. Please refresh the page.');
|
|
228
216
|
}
|
|
229
217
|
}
|
|
230
218
|
|
package/src/ai/analyzer.js
CHANGED
|
@@ -26,10 +26,10 @@ const { buildSparseCheckoutGuidance } = require('./prompts/sparse-checkout-guida
|
|
|
26
26
|
class Analyzer {
|
|
27
27
|
/**
|
|
28
28
|
* @param {Object} database - Database instance
|
|
29
|
-
* @param {string} model - Model to use (e.g., '
|
|
29
|
+
* @param {string} model - Model to use (e.g., 'opus', 'gemini-2.5-pro')
|
|
30
30
|
* @param {string} provider - Provider ID (e.g., 'claude', 'gemini'). Defaults to 'claude'.
|
|
31
31
|
*/
|
|
32
|
-
constructor(database, model = '
|
|
32
|
+
constructor(database, model = 'opus', provider = 'claude') {
|
|
33
33
|
// Store model and provider for creating provider instances per level
|
|
34
34
|
this.model = model;
|
|
35
35
|
this.provider = provider;
|
package/src/ai/claude-cli.js
CHANGED
|
@@ -5,7 +5,7 @@ const logger = require('../utils/logger');
|
|
|
5
5
|
const { extractJSON } = require('../utils/json-extractor');
|
|
6
6
|
|
|
7
7
|
class ClaudeCLI {
|
|
8
|
-
constructor(model = '
|
|
8
|
+
constructor(model = 'opus') {
|
|
9
9
|
// Check for environment variable to override default command
|
|
10
10
|
// Use PAIR_REVIEW_CLAUDE_CMD environment variable if set, otherwise default to 'claude'
|
|
11
11
|
const claudeCmd = process.env.PAIR_REVIEW_CLAUDE_CMD || 'claude';
|
|
@@ -123,6 +123,11 @@ class ClaudeCLI {
|
|
|
123
123
|
}
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
+
// Handle stdin errors (e.g., EPIPE if process exits before write completes)
|
|
127
|
+
claude.stdin.on('error', (err) => {
|
|
128
|
+
logger.error(`${levelPrefix} stdin error: ${err.message}`);
|
|
129
|
+
});
|
|
130
|
+
|
|
126
131
|
// Send the prompt to stdin with backpressure handling
|
|
127
132
|
claude.stdin.write(prompt, (err) => {
|
|
128
133
|
if (err) {
|
|
@@ -22,7 +22,7 @@ const BIN_DIR = path.join(__dirname, '..', '..', 'bin');
|
|
|
22
22
|
const CLAUDE_MODELS = [
|
|
23
23
|
{
|
|
24
24
|
id: 'haiku',
|
|
25
|
-
name: 'Haiku',
|
|
25
|
+
name: 'Haiku 4.5',
|
|
26
26
|
tier: 'fast',
|
|
27
27
|
tagline: 'Lightning Fast',
|
|
28
28
|
description: 'Quick analysis for simple changes',
|
|
@@ -31,21 +31,65 @@ const CLAUDE_MODELS = [
|
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
id: 'sonnet',
|
|
34
|
-
name: 'Sonnet',
|
|
34
|
+
name: 'Sonnet 4.5',
|
|
35
35
|
tier: 'balanced',
|
|
36
36
|
tagline: 'Best Balance',
|
|
37
37
|
description: 'Recommended for most reviews',
|
|
38
|
-
badge: '
|
|
39
|
-
badgeClass: 'badge-recommended'
|
|
40
|
-
|
|
38
|
+
badge: 'Standard',
|
|
39
|
+
badgeClass: 'badge-recommended'
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'opus-4.5',
|
|
43
|
+
cli_model: 'claude-opus-4-5-20251101',
|
|
44
|
+
name: 'Opus 4.5',
|
|
45
|
+
tier: 'balanced',
|
|
46
|
+
tagline: 'Deep Thinker',
|
|
47
|
+
description: 'Extended thinking for complex analysis',
|
|
48
|
+
badge: 'Previous Gen',
|
|
49
|
+
badgeClass: 'badge-power'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'opus-4.6-low',
|
|
53
|
+
cli_model: 'opus',
|
|
54
|
+
env: { CLAUDE_CODE_EFFORT_LEVEL: 'low' },
|
|
55
|
+
name: 'Opus 4.6 Low',
|
|
56
|
+
tier: 'balanced',
|
|
57
|
+
tagline: 'Fast Opus',
|
|
58
|
+
description: 'Opus 4.6 with low effort — quick and capable',
|
|
59
|
+
badge: 'Balanced',
|
|
60
|
+
badgeClass: 'badge-recommended'
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'opus-4.6-medium',
|
|
64
|
+
cli_model: 'opus',
|
|
65
|
+
env: { CLAUDE_CODE_EFFORT_LEVEL: 'medium' },
|
|
66
|
+
name: 'Opus 4.6 Medium',
|
|
67
|
+
tier: 'balanced',
|
|
68
|
+
tagline: 'Balanced Opus',
|
|
69
|
+
description: 'Opus 4.6 with medium effort — balanced depth',
|
|
70
|
+
badge: 'Thorough',
|
|
71
|
+
badgeClass: 'badge-power'
|
|
41
72
|
},
|
|
42
73
|
{
|
|
43
74
|
id: 'opus',
|
|
44
|
-
|
|
75
|
+
aliases: ['opus-4.6-high'],
|
|
76
|
+
env: { CLAUDE_CODE_EFFORT_LEVEL: 'high' },
|
|
77
|
+
name: 'Opus 4.6 High',
|
|
45
78
|
tier: 'thorough',
|
|
46
|
-
tagline: '
|
|
47
|
-
description: '
|
|
79
|
+
tagline: 'Maximum Depth',
|
|
80
|
+
description: 'Opus 4.6 with high effort — deepest analysis',
|
|
48
81
|
badge: 'Most Thorough',
|
|
82
|
+
badgeClass: 'badge-power',
|
|
83
|
+
default: true
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'opus-4.6-1m',
|
|
87
|
+
cli_model: 'opus[1m]',
|
|
88
|
+
name: 'Opus 4.6 1M',
|
|
89
|
+
tier: 'balanced',
|
|
90
|
+
tagline: 'Extended Context',
|
|
91
|
+
description: 'Opus 4.6 high effort with 1M token context window',
|
|
92
|
+
badge: 'More Context',
|
|
49
93
|
badgeClass: 'badge-power'
|
|
50
94
|
}
|
|
51
95
|
];
|
|
@@ -59,7 +103,7 @@ class ClaudeProvider extends AIProvider {
|
|
|
59
103
|
* @param {Object} configOverrides.env - Additional environment variables
|
|
60
104
|
* @param {Object[]} configOverrides.models - Custom model definitions
|
|
61
105
|
*/
|
|
62
|
-
constructor(model = '
|
|
106
|
+
constructor(model = 'opus', configOverrides = {}) {
|
|
63
107
|
super(model);
|
|
64
108
|
|
|
65
109
|
// Command precedence: ENV > config > default
|
|
@@ -67,7 +111,7 @@ class ClaudeProvider extends AIProvider {
|
|
|
67
111
|
const configCmd = configOverrides.command;
|
|
68
112
|
const claudeCmd = envCmd || configCmd || 'claude';
|
|
69
113
|
|
|
70
|
-
// Store for use in getExtractionConfig and testAvailability
|
|
114
|
+
// Store for use in getExtractionConfig, buildArgsForModel, and testAvailability
|
|
71
115
|
this.claudeCmd = claudeCmd;
|
|
72
116
|
this.configOverrides = configOverrides;
|
|
73
117
|
|
|
@@ -77,6 +121,9 @@ class ClaudeProvider extends AIProvider {
|
|
|
77
121
|
// Check for budget limit environment variable
|
|
78
122
|
const maxBudget = process.env.PAIR_REVIEW_MAX_BUDGET_USD;
|
|
79
123
|
|
|
124
|
+
// Resolve model config using shared helper
|
|
125
|
+
const { builtIn, configModel, cliModelArgs, extraArgs, env } = this._resolveModelConfig(model);
|
|
126
|
+
|
|
80
127
|
// Build args: base args + provider extra_args + model extra_args
|
|
81
128
|
// Use --output-format stream-json for JSONL streaming output (better debugging visibility)
|
|
82
129
|
//
|
|
@@ -116,43 +163,98 @@ class ClaudeProvider extends AIProvider {
|
|
|
116
163
|
].join(',');
|
|
117
164
|
permissionArgs = ['--allowedTools', allowedTools];
|
|
118
165
|
}
|
|
119
|
-
const baseArgs = ['-p', '--verbose',
|
|
166
|
+
const baseArgs = ['-p', '--verbose', ...cliModelArgs, '--output-format', 'stream-json', ...permissionArgs];
|
|
120
167
|
if (maxBudget) {
|
|
121
168
|
const budgetNum = parseFloat(maxBudget);
|
|
122
169
|
if (isNaN(budgetNum) || budgetNum <= 0) {
|
|
123
|
-
|
|
170
|
+
logger.warn(`Warning: PAIR_REVIEW_MAX_BUDGET_USD="${maxBudget}" is not a valid positive number, ignoring`);
|
|
124
171
|
} else {
|
|
125
172
|
baseArgs.push('--max-budget-usd', String(budgetNum));
|
|
126
173
|
}
|
|
127
174
|
}
|
|
128
|
-
const providerArgs = configOverrides.extra_args || [];
|
|
129
|
-
const modelConfig = configOverrides.models?.find(m => m.id === model);
|
|
130
|
-
const modelArgs = modelConfig?.extra_args || [];
|
|
131
175
|
|
|
132
|
-
//
|
|
133
|
-
this.extraEnv =
|
|
134
|
-
...(configOverrides.env || {}),
|
|
135
|
-
...(modelConfig?.env || {})
|
|
136
|
-
};
|
|
176
|
+
// Three-way merge for env: built-in model → provider config → per-model config
|
|
177
|
+
this.extraEnv = env;
|
|
137
178
|
|
|
138
179
|
if (this.useShell) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const quotedBaseArgs = baseArgs.map((arg, i) => {
|
|
142
|
-
// The allowedTools value follows the --allowedTools flag
|
|
143
|
-
if (baseArgs[i - 1] === '--allowedTools') {
|
|
144
|
-
return `'${arg}'`;
|
|
145
|
-
}
|
|
146
|
-
return arg;
|
|
147
|
-
});
|
|
148
|
-
this.command = `${claudeCmd} ${[...quotedBaseArgs, ...providerArgs, ...modelArgs].join(' ')}`;
|
|
180
|
+
const allArgs = [...baseArgs, ...extraArgs];
|
|
181
|
+
this.command = `${claudeCmd} ${this._quoteShellArgs(allArgs).join(' ')}`;
|
|
149
182
|
this.args = [];
|
|
150
183
|
} else {
|
|
151
184
|
this.command = claudeCmd;
|
|
152
|
-
this.args = [...baseArgs, ...
|
|
185
|
+
this.args = [...baseArgs, ...extraArgs];
|
|
153
186
|
}
|
|
154
187
|
}
|
|
155
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Quote shell-sensitive arguments for safe shell execution.
|
|
191
|
+
* Any arg containing characters that could be interpreted by the shell
|
|
192
|
+
* (brackets, parentheses, commas, etc.) is wrapped in single quotes
|
|
193
|
+
* with internal single quotes escaped using the POSIX pattern.
|
|
194
|
+
*
|
|
195
|
+
* @param {string[]} args - Array of CLI arguments
|
|
196
|
+
* @returns {string[]} Args with shell-sensitive values quoted
|
|
197
|
+
* @private
|
|
198
|
+
*/
|
|
199
|
+
_quoteShellArgs(args) {
|
|
200
|
+
return args.map((arg, i) => {
|
|
201
|
+
const prevArg = args[i - 1];
|
|
202
|
+
if (prevArg === '--allowedTools' || prevArg === '--model') {
|
|
203
|
+
if (/[][*?(){}$!&|;<>,\s']/.test(arg)) {
|
|
204
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return arg;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Resolve model configuration by looking up built-in and config override definitions.
|
|
213
|
+
* Consolidates the CLAUDE_MODELS.find() and configOverrides.models.find() lookups
|
|
214
|
+
* used across the constructor, buildArgsForModel(), and getExtractionConfig().
|
|
215
|
+
*
|
|
216
|
+
* @param {string} modelId - The model identifier to resolve
|
|
217
|
+
* @returns {Object} Resolved configuration
|
|
218
|
+
* @returns {Object|undefined} .builtIn - Built-in model definition from CLAUDE_MODELS
|
|
219
|
+
* @returns {Object|undefined} .configModel - Config override model definition
|
|
220
|
+
* @returns {string[]} .cliModelArgs - Args array for --model (empty if suppressed)
|
|
221
|
+
* @returns {string[]} .extraArgs - Merged extra_args from built-in, provider, and config model
|
|
222
|
+
* @returns {Object} .env - Merged env from built-in, provider, and config model
|
|
223
|
+
* @private
|
|
224
|
+
*/
|
|
225
|
+
_resolveModelConfig(modelId) {
|
|
226
|
+
const configOverrides = this.configOverrides || {};
|
|
227
|
+
|
|
228
|
+
// Resolve cli_model: config model > built-in model > id
|
|
229
|
+
// cli_model decouples the app-level model ID from the CLI --model argument.
|
|
230
|
+
// - undefined: fall through the resolution chain
|
|
231
|
+
// - string: use this exact value for --model
|
|
232
|
+
// - null: explicitly suppress --model (for tools that want the model set via env instead)
|
|
233
|
+
const builtIn = CLAUDE_MODELS.find(m => m.id === modelId || (m.aliases && m.aliases.includes(modelId)));
|
|
234
|
+
const configModel = configOverrides.models?.find(m => m.id === modelId);
|
|
235
|
+
const resolvedCliModel = configModel?.cli_model !== undefined
|
|
236
|
+
? configModel.cli_model
|
|
237
|
+
: (builtIn?.cli_model !== undefined ? builtIn.cli_model : modelId);
|
|
238
|
+
|
|
239
|
+
// Conditionally include --model in base args (null = suppress, empty string passes through to surface CLI error)
|
|
240
|
+
const cliModelArgs = resolvedCliModel !== null ? ['--model', resolvedCliModel] : [];
|
|
241
|
+
|
|
242
|
+
// Three-way merge for extra_args: built-in model → provider config → per-model config
|
|
243
|
+
const builtInArgs = builtIn?.extra_args || [];
|
|
244
|
+
const providerArgs = configOverrides.extra_args || [];
|
|
245
|
+
const configModelArgs = configModel?.extra_args || [];
|
|
246
|
+
const extraArgs = [...builtInArgs, ...providerArgs, ...configModelArgs];
|
|
247
|
+
|
|
248
|
+
// Three-way merge for env: built-in model → provider config → per-model config
|
|
249
|
+
const env = {
|
|
250
|
+
...(builtIn?.env || {}),
|
|
251
|
+
...(configOverrides.env || {}),
|
|
252
|
+
...(configModel?.env || {})
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
return { builtIn, configModel, cliModelArgs, extraArgs, env };
|
|
256
|
+
}
|
|
257
|
+
|
|
156
258
|
/**
|
|
157
259
|
* Execute Claude CLI with a prompt
|
|
158
260
|
* @param {string} prompt - The prompt to send to Claude
|
|
@@ -328,6 +430,11 @@ class ClaudeProvider extends AIProvider {
|
|
|
328
430
|
}
|
|
329
431
|
});
|
|
330
432
|
|
|
433
|
+
// Handle stdin errors (e.g., EPIPE if process exits before write completes)
|
|
434
|
+
claude.stdin.on('error', (err) => {
|
|
435
|
+
logger.error(`${levelPrefix} stdin error: ${err.message}`);
|
|
436
|
+
});
|
|
437
|
+
|
|
331
438
|
// Send the prompt to stdin
|
|
332
439
|
claude.stdin.write(prompt, (err) => {
|
|
333
440
|
if (err) {
|
|
@@ -351,15 +458,12 @@ class ClaudeProvider extends AIProvider {
|
|
|
351
458
|
* @returns {string[]} Complete args array for the CLI
|
|
352
459
|
*/
|
|
353
460
|
buildArgsForModel(model) {
|
|
461
|
+
const { cliModelArgs, extraArgs } = this._resolveModelConfig(model);
|
|
462
|
+
|
|
354
463
|
// Base args for extraction (simple prompt mode, no tools needed)
|
|
355
|
-
const baseArgs = ['-p',
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
// Model-specific extra_args (from the model config for the given model)
|
|
359
|
-
const modelConfig = this.configOverrides?.models?.find(m => m.id === model);
|
|
360
|
-
const modelArgs = modelConfig?.extra_args || [];
|
|
361
|
-
|
|
362
|
-
return [...baseArgs, ...providerArgs, ...modelArgs];
|
|
464
|
+
const baseArgs = ['-p', ...cliModelArgs];
|
|
465
|
+
|
|
466
|
+
return [...baseArgs, ...extraArgs];
|
|
363
467
|
}
|
|
364
468
|
|
|
365
469
|
/**
|
|
@@ -373,22 +477,26 @@ class ClaudeProvider extends AIProvider {
|
|
|
373
477
|
const claudeCmd = this.claudeCmd;
|
|
374
478
|
const useShell = this.useShell;
|
|
375
479
|
|
|
376
|
-
//
|
|
377
|
-
const
|
|
480
|
+
// Single call to _resolveModelConfig for both args and env
|
|
481
|
+
const { cliModelArgs, extraArgs, env } = this._resolveModelConfig(model);
|
|
482
|
+
const args = ['-p', ...cliModelArgs, ...extraArgs];
|
|
378
483
|
|
|
379
484
|
if (useShell) {
|
|
485
|
+
const quotedArgs = this._quoteShellArgs(args);
|
|
380
486
|
return {
|
|
381
|
-
command: `${claudeCmd} ${
|
|
487
|
+
command: `${claudeCmd} ${quotedArgs.join(' ')}`,
|
|
382
488
|
args: [],
|
|
383
489
|
useShell: true,
|
|
384
|
-
promptViaStdin: true
|
|
490
|
+
promptViaStdin: true,
|
|
491
|
+
env
|
|
385
492
|
};
|
|
386
493
|
}
|
|
387
494
|
return {
|
|
388
495
|
command: claudeCmd,
|
|
389
496
|
args,
|
|
390
497
|
useShell: false,
|
|
391
|
-
promptViaStdin: true
|
|
498
|
+
promptViaStdin: true,
|
|
499
|
+
env
|
|
392
500
|
};
|
|
393
501
|
}
|
|
394
502
|
|
|
@@ -729,7 +837,7 @@ class ClaudeProvider extends AIProvider {
|
|
|
729
837
|
}
|
|
730
838
|
|
|
731
839
|
static getDefaultModel() {
|
|
732
|
-
return '
|
|
840
|
+
return 'opus';
|
|
733
841
|
}
|
|
734
842
|
|
|
735
843
|
static getInstallInstructions() {
|
package/src/ai/codex-provider.js
CHANGED
|
@@ -305,6 +305,11 @@ class CodexProvider extends AIProvider {
|
|
|
305
305
|
}
|
|
306
306
|
});
|
|
307
307
|
|
|
308
|
+
// Handle stdin errors (e.g., EPIPE if process exits before write completes)
|
|
309
|
+
codex.stdin.on('error', (err) => {
|
|
310
|
+
logger.error(`${levelPrefix} stdin error: ${err.message}`);
|
|
311
|
+
});
|
|
312
|
+
|
|
308
313
|
// Send the prompt to stdin
|
|
309
314
|
codex.stdin.write(prompt, (err) => {
|
|
310
315
|
if (err) {
|
|
@@ -352,6 +352,11 @@ class CursorAgentProvider extends AIProvider {
|
|
|
352
352
|
}
|
|
353
353
|
});
|
|
354
354
|
|
|
355
|
+
// Handle stdin errors (e.g., EPIPE if process exits before write completes)
|
|
356
|
+
agent.stdin.on('error', (err) => {
|
|
357
|
+
logger.error(`${levelPrefix} stdin error: ${err.message}`);
|
|
358
|
+
});
|
|
359
|
+
|
|
355
360
|
// Send the prompt to stdin
|
|
356
361
|
agent.stdin.write(prompt, (err) => {
|
|
357
362
|
if (err) {
|
|
@@ -354,6 +354,11 @@ class GeminiProvider extends AIProvider {
|
|
|
354
354
|
}
|
|
355
355
|
});
|
|
356
356
|
|
|
357
|
+
// Handle stdin errors (e.g., EPIPE if process exits before write completes)
|
|
358
|
+
gemini.stdin.on('error', (err) => {
|
|
359
|
+
logger.error(`${levelPrefix} stdin error: ${err.message}`);
|
|
360
|
+
});
|
|
361
|
+
|
|
357
362
|
// Send the prompt to stdin
|
|
358
363
|
gemini.stdin.write(prompt, (err) => {
|
|
359
364
|
if (err) {
|
|
@@ -289,6 +289,11 @@ class OpenCodeProvider extends AIProvider {
|
|
|
289
289
|
}
|
|
290
290
|
});
|
|
291
291
|
|
|
292
|
+
// Handle stdin errors (e.g., EPIPE if process exits before write completes)
|
|
293
|
+
opencode.stdin.on('error', (err) => {
|
|
294
|
+
logger.error(`${levelPrefix} stdin error: ${err.message}`);
|
|
295
|
+
});
|
|
296
|
+
|
|
292
297
|
// Send the prompt to stdin (OpenCode reads from stdin when no positional args)
|
|
293
298
|
// Note on error handling: When stdin.write fails, we kill the process which
|
|
294
299
|
// triggers the 'close' event handler. The `settled` guard (line 142) prevents
|
package/src/ai/provider.js
CHANGED
|
@@ -192,7 +192,7 @@ class AIProvider {
|
|
|
192
192
|
};
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
const { command, args, useShell, promptViaStdin } = config;
|
|
195
|
+
const { command, args, useShell, promptViaStdin, env: configEnv } = config;
|
|
196
196
|
const prompt = `Extract the JSON object from the following text. Return ONLY the valid JSON, nothing else. Do not include any explanation, markdown formatting, or code blocks - just the raw JSON.
|
|
197
197
|
|
|
198
198
|
=== BEGIN INPUT TEXT ===
|
|
@@ -209,6 +209,7 @@ ${rawResponse}
|
|
|
209
209
|
cwd: process.cwd(),
|
|
210
210
|
env: {
|
|
211
211
|
...process.env,
|
|
212
|
+
...(configEnv || {}),
|
|
212
213
|
PATH: `${BIN_DIR}:${process.env.PATH}`
|
|
213
214
|
},
|
|
214
215
|
shell: useShell
|
|
@@ -279,6 +280,11 @@ ${rawResponse}
|
|
|
279
280
|
|
|
280
281
|
// Send prompt via stdin if configured
|
|
281
282
|
if (promptViaStdin) {
|
|
283
|
+
// Handle stdin errors (e.g., EPIPE if process exits before write completes)
|
|
284
|
+
proc.stdin.on('error', (err) => {
|
|
285
|
+
logger.warn(`${levelPrefix} extraction stdin error: ${err.message}`);
|
|
286
|
+
});
|
|
287
|
+
|
|
282
288
|
proc.stdin.write(prompt, (err) => {
|
|
283
289
|
if (err) {
|
|
284
290
|
logger.warn(`${levelPrefix} Failed to write extraction prompt: ${err}`);
|
package/src/config.js
CHANGED
|
@@ -14,7 +14,7 @@ const DEFAULT_CONFIG = {
|
|
|
14
14
|
port: 7247,
|
|
15
15
|
theme: "light",
|
|
16
16
|
default_provider: "claude", // AI provider: 'claude', 'gemini', 'codex', 'copilot', 'opencode'
|
|
17
|
-
default_model: "
|
|
17
|
+
default_model: "opus", // Model within the provider (e.g., 'opus' for Claude, 'gemini-2.5-pro' for Gemini)
|
|
18
18
|
worktree_retention_days: 7,
|
|
19
19
|
dev_mode: false, // When true, disables static file caching for development
|
|
20
20
|
debug_stream: false, // When true, logs AI provider streaming events (equivalent to --debug-stream CLI flag)
|
package/src/main.js
CHANGED
|
@@ -110,6 +110,7 @@ OPTIONS:
|
|
|
110
110
|
The web UI also starts for the human reviewer.
|
|
111
111
|
--model <name> Override the AI model. Claude Code is the default provider.
|
|
112
112
|
Available models: opus, sonnet, haiku (Claude Code);
|
|
113
|
+
also: opus-4.5, opus-4.6-low, opus-4.6-medium, opus-4.6-1m
|
|
113
114
|
or use provider-specific models with Gemini/Codex
|
|
114
115
|
--use-checkout Use current directory instead of creating worktree
|
|
115
116
|
(automatic in GitHub Actions)
|
|
@@ -129,7 +130,7 @@ ENVIRONMENT VARIABLES:
|
|
|
129
130
|
PAIR_REVIEW_CLAUDE_CMD Custom command to invoke Claude CLI (default: claude)
|
|
130
131
|
PAIR_REVIEW_GEMINI_CMD Custom command to invoke Gemini CLI (default: gemini)
|
|
131
132
|
PAIR_REVIEW_CODEX_CMD Custom command to invoke Codex CLI (default: codex)
|
|
132
|
-
PAIR_REVIEW_MODEL Override the AI model (same as --model flag)
|
|
133
|
+
PAIR_REVIEW_MODEL Override the AI model (same as --model flag, default: opus)
|
|
133
134
|
|
|
134
135
|
CONFIGURATION:
|
|
135
136
|
Config file: ~/.pair-review/config.json
|
|
@@ -852,7 +853,7 @@ async function performHeadlessReview(args, config, db, flags, options) {
|
|
|
852
853
|
|
|
853
854
|
// Run AI analysis
|
|
854
855
|
console.log('Running AI analysis (all 3 levels)...');
|
|
855
|
-
const model = flags.model || process.env.PAIR_REVIEW_MODEL || '
|
|
856
|
+
const model = flags.model || process.env.PAIR_REVIEW_MODEL || 'opus';
|
|
856
857
|
const analyzer = new Analyzer(db, model);
|
|
857
858
|
|
|
858
859
|
let analysisSummary = null;
|
package/src/routes/mcp.js
CHANGED
|
@@ -527,7 +527,7 @@ function createMCPServer(db, options = {}) {
|
|
|
527
527
|
// Resolve provider and model
|
|
528
528
|
const repoSettings = repository ? await repoSettingsRepo.getRepoSettings(repository) : null;
|
|
529
529
|
const provider = process.env.PAIR_REVIEW_PROVIDER || repoSettings?.default_provider || config.default_provider || config.provider || 'claude';
|
|
530
|
-
const model = process.env.PAIR_REVIEW_MODEL || repoSettings?.default_model || config.default_model || config.model || '
|
|
530
|
+
const model = process.env.PAIR_REVIEW_MODEL || repoSettings?.default_model || config.default_model || config.model || 'opus';
|
|
531
531
|
|
|
532
532
|
// Create unified run/analysis ID and DB record immediately
|
|
533
533
|
const runId = uuidv4();
|
|
@@ -676,7 +676,7 @@ function createMCPServer(db, options = {}) {
|
|
|
676
676
|
// Resolve provider and model
|
|
677
677
|
const repoSettings = await repoSettingsRepo.getRepoSettings(repository);
|
|
678
678
|
const provider = process.env.PAIR_REVIEW_PROVIDER || repoSettings?.default_provider || config.default_provider || config.provider || 'claude';
|
|
679
|
-
const model = process.env.PAIR_REVIEW_MODEL || repoSettings?.default_model || config.default_model || config.model || '
|
|
679
|
+
const model = process.env.PAIR_REVIEW_MODEL || repoSettings?.default_model || config.default_model || config.model || 'opus';
|
|
680
680
|
|
|
681
681
|
// Create unified run/analysis ID and DB record immediately
|
|
682
682
|
const runId = uuidv4();
|
package/src/routes/shared.js
CHANGED
|
@@ -70,7 +70,7 @@ function getLocalReviewKey(reviewId) {
|
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
72
|
* Get the model to use for AI analysis
|
|
73
|
-
* Priority: CLI flag (PAIR_REVIEW_MODEL env var) > config.default_model > '
|
|
73
|
+
* Priority: CLI flag (PAIR_REVIEW_MODEL env var) > config.default_model > 'opus' default
|
|
74
74
|
* @param {Object} req - Express request object
|
|
75
75
|
* @returns {string} Model name to use
|
|
76
76
|
*/
|
|
@@ -93,7 +93,7 @@ function getModel(req) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
// Default fallback
|
|
96
|
-
return '
|
|
96
|
+
return 'opus';
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/**
|