@in-the-loop-labs/pair-review 1.4.1 → 1.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@in-the-loop-labs/pair-review",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "description": "Your AI-powered code review partner - Close the feedback loop with AI coding agents",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pair-review",
3
- "version": "1.4.1",
3
+ "version": "1.4.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.4.1",
3
+ "version": "1.4.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",
@@ -460,7 +460,9 @@
460
460
  // ─── Local Review Start ─────────────────────────────────────────────────────
461
461
 
462
462
  /**
463
- * Handle start local review form submission
463
+ * Handle start local review form submission.
464
+ * Navigates to the setup page which shows step-by-step progress,
465
+ * matching the flow used when reviews are started from the MCP/CLI.
464
466
  * @param {Event} event - Form submit event
465
467
  */
466
468
  async function handleStartLocal(event) {
@@ -479,29 +481,9 @@
479
481
  return;
480
482
  }
481
483
 
482
- setFormLoading('local', true, 'Starting local review...');
483
-
484
- try {
485
- const response = await fetch('/api/local/start', {
486
- method: 'POST',
487
- headers: { 'Content-Type': 'application/json' },
488
- body: JSON.stringify({ path: pathValue })
489
- });
490
-
491
- const data = await response.json();
492
-
493
- if (!response.ok) {
494
- throw new Error(data.error || 'Failed to start local review');
495
- }
496
-
497
- setFormLoading('local', true, 'Redirecting to review...');
498
- window.location.href = data.reviewUrl;
499
-
500
- } catch (error) {
501
- console.error('Error starting local review:', error);
502
- setFormLoading('local', false);
503
- showError('local', error.message || 'An unexpected error occurred. Please try again.');
504
- }
484
+ // Navigate to the setup page which shows step-by-step progress
485
+ // The /local?path= route serves setup.html which handles the full setup flow
486
+ window.location.href = '/local?path=' + encodeURIComponent(pathValue);
505
487
  }
506
488
 
507
489
  // ─── Browse Directory ──────────────────────────────────────────────────────
@@ -849,7 +831,10 @@
849
831
  }
850
832
 
851
833
  /**
852
- * Handle start review form submission
834
+ * Handle start review form submission.
835
+ * Parses the PR URL, then navigates to the PR route which serves the
836
+ * setup page with step-by-step progress for new PRs, or the review page
837
+ * directly for PRs that already exist in the database.
853
838
  * @param {Event} event - Form submit event
854
839
  */
855
840
  async function handleStartReview(event) {
@@ -881,44 +866,9 @@
881
866
  return;
882
867
  }
883
868
 
884
- // Update loading state
885
- setFormLoading('pr', true, 'Fetching PR data from GitHub...');
886
-
887
- try {
888
- // Call the API to create the worktree
889
- const response = await fetch('/api/worktrees/create', {
890
- method: 'POST',
891
- headers: {
892
- 'Content-Type': 'application/json'
893
- },
894
- body: JSON.stringify({
895
- owner: parsed.owner,
896
- repo: parsed.repo,
897
- prNumber: parsed.prNumber
898
- })
899
- });
900
-
901
- const data = await response.json();
902
-
903
- if (!response.ok) {
904
- throw new Error(data.error || 'Failed to create worktree');
905
- }
906
-
907
- if (!data.success) {
908
- throw new Error(data.error || 'Failed to create worktree');
909
- }
910
-
911
- // Update loading text before redirect
912
- setFormLoading('pr', true, 'Redirecting to review...');
913
-
914
- // Redirect to the review page
915
- window.location.href = data.reviewUrl;
916
-
917
- } catch (error) {
918
- console.error('Error starting review:', error);
919
- setFormLoading('pr', false);
920
- showError('pr', error.message || 'An unexpected error occurred. Please try again.');
921
- }
869
+ // Navigate to the PR route which serves setup.html (with step-by-step progress)
870
+ // for new PRs, or pr.html directly for PRs already in the database
871
+ window.location.href = '/pr/' + encodeURIComponent(parsed.owner) + '/' + encodeURIComponent(parsed.repo) + '/' + encodeURIComponent(parsed.prNumber);
922
872
  }
923
873
 
924
874
  // ─── Config & Command Examples ──────────────────────────────────────────────
@@ -1068,4 +1018,17 @@
1068
1018
  // natively triggers form submission.
1069
1019
  });
1070
1020
 
1021
+ // ─── bfcache Restoration ───────────────────────────────────────────────────
1022
+
1023
+ // When the browser restores this page from bfcache (e.g. user hits the back
1024
+ // button after navigating away), any in-progress loading state on the forms
1025
+ // will still be visible because the DOM snapshot is preserved as-is. Reset
1026
+ // both forms so the user is not stuck with a disabled input and spinner.
1027
+ window.addEventListener('pageshow', function (event) {
1028
+ if (event.persisted) {
1029
+ setFormLoading('pr', false);
1030
+ setFormLoading('local', false);
1031
+ }
1032
+ });
1033
+
1071
1034
  })();
@@ -7,7 +7,7 @@
7
7
 
8
8
  const path = require('path');
9
9
  const { spawn } = require('child_process');
10
- const { AIProvider, registerProvider } = require('./provider');
10
+ const { AIProvider, registerProvider, quoteShellArgs } = require('./provider');
11
11
  const logger = require('../utils/logger');
12
12
  const { extractJSON } = require('../utils/json-extractor');
13
13
  const { CancellationError, isAnalysisCancelled } = require('../routes/shared');
@@ -178,7 +178,7 @@ class ClaudeProvider extends AIProvider {
178
178
 
179
179
  if (this.useShell) {
180
180
  const allArgs = [...baseArgs, ...extraArgs];
181
- this.command = `${claudeCmd} ${this._quoteShellArgs(allArgs).join(' ')}`;
181
+ this.command = `${claudeCmd} ${quoteShellArgs(allArgs).join(' ')}`;
182
182
  this.args = [];
183
183
  } else {
184
184
  this.command = claudeCmd;
@@ -186,28 +186,6 @@ class ClaudeProvider extends AIProvider {
186
186
  }
187
187
  }
188
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
189
  /**
212
190
  * Resolve model configuration by looking up built-in and config override definitions.
213
191
  * Consolidates the CLAUDE_MODELS.find() and configOverrides.models.find() lookups
@@ -486,7 +464,7 @@ class ClaudeProvider extends AIProvider {
486
464
  const args = ['-p', ...cliModelArgs, ...extraArgs];
487
465
 
488
466
  if (useShell) {
489
- const quotedArgs = this._quoteShellArgs(args);
467
+ const quotedArgs = quoteShellArgs(args);
490
468
  return {
491
469
  command: `${claudeCmd} ${quotedArgs.join(' ')}`,
492
470
  args: [],
@@ -8,7 +8,7 @@
8
8
 
9
9
  const path = require('path');
10
10
  const { spawn } = require('child_process');
11
- const { AIProvider, registerProvider } = require('./provider');
11
+ const { AIProvider, registerProvider, quoteShellArgs } = require('./provider');
12
12
  const logger = require('../utils/logger');
13
13
  const { extractJSON } = require('../utils/json-extractor');
14
14
  const { CancellationError, isAnalysisCancelled } = require('../routes/shared');
@@ -22,8 +22,8 @@ const BIN_DIR = path.join(__dirname, '..', '..', 'bin');
22
22
  *
23
23
  * Based on OpenAI Codex Models guide (developers.openai.com/codex/models)
24
24
  * - gpt-5.1-codex-mini: Smaller, cost-effective variant for quick scans
25
- * - gpt-5.1-codex-max: Optimized for long-horizon agentic coding tasks
26
- * - gpt-5.2-codex: Most advanced agentic coding model for real-world engineering
25
+ * - gpt-5.2-codex: Advanced coding model for everyday reviews, good reasoning/cost balance
26
+ * - gpt-5.3-codex: Most capable agentic coding model with frontier performance and reasoning
27
27
  */
28
28
  const CODEX_MODELS = [
29
29
  {
@@ -36,21 +36,21 @@ const CODEX_MODELS = [
36
36
  badgeClass: 'badge-speed'
37
37
  },
38
38
  {
39
- id: 'gpt-5.1-codex-max',
40
- name: 'GPT-5.1 Max',
39
+ id: 'gpt-5.2-codex',
40
+ name: 'GPT-5.2 Codex',
41
41
  tier: 'balanced',
42
42
  tagline: 'Best Balance',
43
- description: 'Strong everyday reviewer—quality + speed for PR-sized changes and practical suggestions.',
43
+ description: 'Strong everyday reviewer—good reasoning and code understanding for PR-sized changes without top-tier cost.',
44
44
  badge: 'Recommended',
45
45
  badgeClass: 'badge-recommended',
46
46
  default: true
47
47
  },
48
48
  {
49
- id: 'gpt-5.2-codex',
50
- name: 'GPT-5.2',
49
+ id: 'gpt-5.3-codex',
50
+ name: 'GPT-5.3 Codex',
51
51
  tier: 'thorough',
52
52
  tagline: 'Deep Review',
53
- description: 'Most capable for complex diffsfinds subtle issues, reasons across files, and proposes step-by-step fixes.',
53
+ description: 'Most capable agentic coding modelcombines frontier coding performance with stronger reasoning for deep cross-file analysis.',
54
54
  badge: 'Most Thorough',
55
55
  badgeClass: 'badge-power'
56
56
  }
@@ -65,7 +65,7 @@ class CodexProvider extends AIProvider {
65
65
  * @param {Object} configOverrides.env - Additional environment variables
66
66
  * @param {Object[]} configOverrides.models - Custom model definitions
67
67
  */
68
- constructor(model = 'gpt-5.1-codex-max', configOverrides = {}) {
68
+ constructor(model = 'gpt-5.2-codex', configOverrides = {}) {
69
69
  super(model);
70
70
 
71
71
  // Command precedence: ENV > config > default
@@ -116,7 +116,7 @@ class CodexProvider extends AIProvider {
116
116
 
117
117
  if (this.useShell) {
118
118
  // In shell mode, build full command string with args
119
- this.command = `${codexCmd} ${[...baseArgs, ...providerArgs, ...modelArgs].join(' ')}`;
119
+ this.command = `${codexCmd} ${quoteShellArgs([...baseArgs, ...providerArgs, ...modelArgs]).join(' ')}`;
120
120
  this.args = [];
121
121
  } else {
122
122
  this.command = codexCmd;
@@ -577,7 +577,7 @@ class CodexProvider extends AIProvider {
577
577
 
578
578
  if (useShell) {
579
579
  return {
580
- command: `${codexCmd} ${args.join(' ')}`,
580
+ command: `${codexCmd} ${quoteShellArgs(args).join(' ')}`,
581
581
  args: [],
582
582
  useShell: true,
583
583
  promptViaStdin: true
@@ -676,7 +676,7 @@ class CodexProvider extends AIProvider {
676
676
  }
677
677
 
678
678
  static getDefaultModel() {
679
- return 'gpt-5.1-codex-max';
679
+ return 'gpt-5.2-codex';
680
680
  }
681
681
 
682
682
  static getInstallInstructions() {
@@ -8,7 +8,7 @@
8
8
 
9
9
  const path = require('path');
10
10
  const { spawn } = require('child_process');
11
- const { AIProvider, registerProvider } = require('./provider');
11
+ const { AIProvider, registerProvider, quoteShellArgs } = require('./provider');
12
12
  const logger = require('../utils/logger');
13
13
  const { extractJSON } = require('../utils/json-extractor');
14
14
  const { CancellationError, isAnalysisCancelled } = require('../routes/shared');
@@ -21,42 +21,63 @@ const BIN_DIR = path.join(__dirname, '..', '..', 'bin');
21
21
  *
22
22
  * GitHub Copilot CLI supports multiple AI models including OpenAI,
23
23
  * Anthropic, and Google models via the --model flag.
24
+ * Available models (as of Feb 2026): claude-haiku-4.5, claude-sonnet-4.5,
25
+ * gemini-3-pro-preview, gpt-5.2-codex, claude-opus-4.5,
26
+ * claude-opus-4.6. Default is claude-sonnet-4.5.
24
27
  */
25
28
  const COPILOT_MODELS = [
26
29
  {
27
- id: 'gpt-5.1-codex-mini',
28
- name: 'GPT-5.1 Mini',
30
+ id: 'claude-haiku-4.5',
31
+ name: 'Claude Haiku 4.5',
29
32
  tier: 'fast',
30
33
  tagline: 'Quick Scan',
31
- description: 'Rapid feedback for obvious issues and style checks',
34
+ description: 'Rapid feedback for obvious issues, style checks, and simple logic errors',
32
35
  badge: 'Speedy',
33
36
  badgeClass: 'badge-speed'
34
37
  },
35
38
  {
36
- id: 'gemini-3-pro-preview',
37
- name: 'Gemini 3 Pro',
39
+ id: 'claude-sonnet-4.5',
40
+ name: 'Claude Sonnet 4.5',
38
41
  tier: 'balanced',
39
42
  tagline: 'Reliable Review',
40
- description: 'Solid everyday reviews with good coverage',
43
+ description: 'Copilot default—strong code understanding with excellent quality-to-cost ratio',
41
44
  badge: 'Recommended',
42
45
  badgeClass: 'badge-recommended',
43
46
  default: true
44
47
  },
45
48
  {
46
- id: 'gpt-5.1-codex-max',
47
- name: 'GPT-5.1 Max',
48
- tier: 'thorough',
49
- tagline: 'Deep Analysis',
50
- description: 'Comprehensive reviews for complex changes',
51
- badge: 'Thorough',
52
- badgeClass: 'badge-power'
49
+ id: 'gemini-3-pro-preview',
50
+ name: 'Gemini 3 Pro',
51
+ tier: 'balanced',
52
+ tagline: 'Strong Alternative',
53
+ description: "Google's most capable model—strong reasoning for cross-file analysis",
54
+ badge: 'Balanced',
55
+ badgeClass: 'badge-balanced'
56
+ },
57
+ {
58
+ id: 'gpt-5.2-codex',
59
+ name: 'GPT-5.2 Codex',
60
+ tier: 'balanced',
61
+ tagline: 'Alternative View',
62
+ description: 'OpenAI code-specialized model—different perspective for cross-file analysis',
63
+ badge: 'Balanced',
64
+ badgeClass: 'badge-balanced'
53
65
  },
54
66
  {
55
67
  id: 'claude-opus-4.5',
56
68
  name: 'Claude Opus 4.5',
57
- tier: 'premium',
58
- tagline: 'Ultimate Review',
59
- description: 'The most capable model for critical code reviews',
69
+ tier: 'thorough',
70
+ tagline: 'Deep Analysis',
71
+ description: 'Highly capable model for critical code reviews—strong reasoning for security and architecture',
72
+ badge: 'Premium',
73
+ badgeClass: 'badge-premium'
74
+ },
75
+ {
76
+ id: 'claude-opus-4.6',
77
+ name: 'Claude Opus 4.6',
78
+ tier: 'thorough',
79
+ tagline: 'Most Capable',
80
+ description: 'Most capable model for critical code reviews—deep reasoning for security and architecture',
60
81
  badge: 'Premium',
61
82
  badgeClass: 'badge-premium'
62
83
  }
@@ -71,7 +92,7 @@ class CopilotProvider extends AIProvider {
71
92
  * @param {Object} configOverrides.env - Additional environment variables
72
93
  * @param {Object[]} configOverrides.models - Custom model definitions
73
94
  */
74
- constructor(model = 'gemini-3-pro-preview', configOverrides = {}) {
95
+ constructor(model = 'claude-sonnet-4.5', configOverrides = {}) {
75
96
  super(model);
76
97
 
77
98
  // Command precedence: ENV > config > default
@@ -191,7 +212,7 @@ class CopilotProvider extends AIProvider {
191
212
  // Escape the prompt for shell
192
213
  const escapedPrompt = prompt.replace(/'/g, "'\\''");
193
214
  // Build: copilot --model X --deny-tool ... -s -p 'prompt'
194
- fullCommand = `${this.command} ${this.baseArgs.join(' ')} -p '${escapedPrompt}'`;
215
+ fullCommand = `${this.command} ${quoteShellArgs(this.baseArgs).join(' ')} -p '${escapedPrompt}'`;
195
216
  fullArgs = [];
196
217
  } else {
197
218
  // Build args array: --model X --deny-tool ... -s -p <prompt>
@@ -359,7 +380,7 @@ class CopilotProvider extends AIProvider {
359
380
  // Use stdin for prompt - safer than command args for arbitrary content
360
381
  if (useShell) {
361
382
  return {
362
- command: `${copilotCmd} ${args.join(' ')}`,
383
+ command: `${copilotCmd} ${quoteShellArgs(args).join(' ')}`,
363
384
  args: [],
364
385
  useShell: true,
365
386
  promptViaStdin: true
@@ -441,7 +462,7 @@ class CopilotProvider extends AIProvider {
441
462
  }
442
463
 
443
464
  static getDefaultModel() {
444
- return 'gemini-3-pro-preview';
465
+ return 'claude-sonnet-4.5';
445
466
  }
446
467
 
447
468
  static getInstallInstructions() {
@@ -16,7 +16,7 @@
16
16
 
17
17
  const path = require('path');
18
18
  const { spawn } = require('child_process');
19
- const { AIProvider, registerProvider } = require('./provider');
19
+ const { AIProvider, registerProvider, quoteShellArgs } = require('./provider');
20
20
  const logger = require('../utils/logger');
21
21
  const { extractJSON } = require('../utils/json-extractor');
22
22
  const { CancellationError, isAnalysisCancelled } = require('../routes/shared');
@@ -30,9 +30,9 @@ const BIN_DIR = path.join(__dirname, '..', '..', 'bin');
30
30
  *
31
31
  * Tier structure:
32
32
  * - free (auto): Cursor's default auto-routing model
33
- * - fast (gpt-5.2-codex-fast): Quick code-specialized analysis
34
- * - balanced (sonnet-4.5-thinking, gemini-3-pro): Recommended for most reviews
35
- * - thorough (gpt-5.2-codex-high, opus-4.5-thinking): Deep analysis for complex code
33
+ * - fast (composer-1, gpt-5.3-codex-fast, gemini-3-flash): Quick analysis
34
+ * - balanced (composer-1.5, sonnet-4.5-thinking, gemini-3-pro): Recommended for most reviews
35
+ * - thorough (gpt-5.3-codex-high, opus-4.5-thinking, opus-4.6-thinking): Deep analysis for complex code
36
36
  */
37
37
  const CURSOR_AGENT_MODELS = [
38
38
  {
@@ -45,20 +45,47 @@ const CURSOR_AGENT_MODELS = [
45
45
  badgeClass: 'badge-speed'
46
46
  },
47
47
  {
48
- id: 'gpt-5.2-codex-fast',
49
- name: 'GPT-5.2 Codex Fast',
48
+ id: 'composer-1.5',
49
+ name: 'Composer 1.5',
50
+ tier: 'balanced',
51
+ tagline: 'Latest Composer',
52
+ description: 'Cursor Composer model—positioned between Sonnet and Opus for multi-file edits',
53
+ badge: 'Balanced',
54
+ badgeClass: 'badge-balanced'
55
+ },
56
+ {
57
+ id: 'composer-1',
58
+ name: 'Composer 1',
59
+ tier: 'fast',
60
+ tagline: 'Original Composer',
61
+ description: 'Cursor Composer model—good for quick multi-file editing workflows',
62
+ badge: 'Fast',
63
+ badgeClass: 'badge-speed'
64
+ },
65
+ {
66
+ id: 'gpt-5.3-codex-fast',
67
+ name: 'GPT-5.3 Codex Fast',
50
68
  tier: 'fast',
51
69
  tagline: 'Lightning Fast',
52
- description: 'Quick code-specialized analysis for simple changes',
70
+ description: 'Latest code-specialized model optimized for speed—quick scans for obvious issues',
53
71
  badge: 'Fastest',
54
72
  badgeClass: 'badge-speed'
55
73
  },
74
+ {
75
+ id: 'gemini-3-flash',
76
+ name: 'Gemini 3 Flash',
77
+ tier: 'fast',
78
+ tagline: 'Fast & Capable',
79
+ description: 'High SWE-bench scores at a fraction of the cost—great for quick reviews',
80
+ badge: 'Fast',
81
+ badgeClass: 'badge-speed'
82
+ },
56
83
  {
57
84
  id: 'sonnet-4.5-thinking',
58
85
  name: 'Claude 4.5 Sonnet (Thinking)',
59
86
  tier: 'balanced',
60
87
  tagline: 'Best Balance',
61
- description: 'Extended thinking for thorough analysis',
88
+ description: 'Extended thinking for thorough analysis with excellent quality-to-cost ratio',
62
89
  badge: 'Recommended',
63
90
  badgeClass: 'badge-recommended',
64
91
  default: true
@@ -68,16 +95,16 @@ const CURSOR_AGENT_MODELS = [
68
95
  name: 'Gemini 3 Pro',
69
96
  tier: 'balanced',
70
97
  tagline: 'Strong Alternative',
71
- description: "Google's flagship model for code review",
98
+ description: "Google's flagship model for code review—strong agentic and vibe coding capabilities",
72
99
  badge: 'Balanced',
73
100
  badgeClass: 'badge-balanced'
74
101
  },
75
102
  {
76
- id: 'gpt-5.2-codex-high',
77
- name: 'GPT-5.2 Codex High',
103
+ id: 'gpt-5.3-codex-high',
104
+ name: 'GPT-5.3 Codex High',
78
105
  tier: 'thorough',
79
106
  tagline: 'Deep Code Analysis',
80
- description: "OpenAI's best for complex code review",
107
+ description: "OpenAI's latest and most capable for complex code review with deep reasoning",
81
108
  badge: 'Thorough',
82
109
  badgeClass: 'badge-power'
83
110
  },
@@ -85,8 +112,17 @@ const CURSOR_AGENT_MODELS = [
85
112
  id: 'opus-4.5-thinking',
86
113
  name: 'Claude 4.5 Opus (Thinking)',
87
114
  tier: 'thorough',
115
+ tagline: 'Deep Analysis',
116
+ description: 'Deep analysis with extended thinking for complex code reviews',
117
+ badge: 'Thorough',
118
+ badgeClass: 'badge-power'
119
+ },
120
+ {
121
+ id: 'opus-4.6-thinking',
122
+ name: 'Claude 4.6 Opus (Thinking)',
123
+ tier: 'thorough',
88
124
  tagline: 'Most Capable',
89
- description: 'Deep analysis with extended thinking for complex code',
125
+ description: 'Deep analysis with extended thinking—Cursor default for maximum review quality',
90
126
  badge: 'Most Thorough',
91
127
  badgeClass: 'badge-power'
92
128
  }
@@ -159,7 +195,7 @@ class CursorAgentProvider extends AIProvider {
159
195
 
160
196
  if (this.useShell) {
161
197
  // In shell mode, build full command string with args
162
- this.command = `${agentCmd} ${[...baseArgs, ...providerArgs, ...modelArgs].join(' ')}`;
198
+ this.command = `${agentCmd} ${quoteShellArgs([...baseArgs, ...providerArgs, ...modelArgs]).join(' ')}`;
163
199
  this.args = [];
164
200
  } else {
165
201
  this.command = agentCmd;
@@ -662,7 +698,7 @@ class CursorAgentProvider extends AIProvider {
662
698
  // For extraction, we pass the prompt via stdin
663
699
  if (useShell) {
664
700
  return {
665
- command: `${agentCmd} ${args.join(' ')}`,
701
+ command: `${agentCmd} ${quoteShellArgs(args).join(' ')}`,
666
702
  args: [],
667
703
  useShell: true,
668
704
  promptViaStdin: true
@@ -7,7 +7,7 @@
7
7
 
8
8
  const path = require('path');
9
9
  const { spawn } = require('child_process');
10
- const { AIProvider, registerProvider } = require('./provider');
10
+ const { AIProvider, registerProvider, quoteShellArgs } = require('./provider');
11
11
  const logger = require('../utils/logger');
12
12
  const { extractJSON } = require('../utils/json-extractor');
13
13
  const { CancellationError, isAnalysisCancelled } = require('../routes/shared');
@@ -25,7 +25,7 @@ const GEMINI_MODELS = [
25
25
  name: '3.0 Flash',
26
26
  tier: 'fast',
27
27
  tagline: 'Rapid Sanity Check',
28
- description: 'Best for catching syntax, typos, and simple logic errors',
28
+ description: 'Fast and capable at a fraction of the cost of larger models',
29
29
  badge: 'Quick Look',
30
30
  badgeClass: 'badge-speed'
31
31
  },
@@ -34,7 +34,7 @@ const GEMINI_MODELS = [
34
34
  name: '2.5 Pro',
35
35
  tier: 'balanced',
36
36
  tagline: 'Standard PR Review',
37
- description: 'Reliable feedback on code style, features, and refactoring',
37
+ description: 'Strong reasoning with large context window—reliable for everyday code reviews',
38
38
  badge: 'Daily Driver',
39
39
  badgeClass: 'badge-recommended',
40
40
  default: true
@@ -44,7 +44,7 @@ const GEMINI_MODELS = [
44
44
  name: '3.0 Pro',
45
45
  tier: 'thorough',
46
46
  tagline: 'Architectural Audit',
47
- description: 'Deep analysis for race conditions, security, and edge cases',
47
+ description: 'Most intelligent Gemini model—advanced reasoning for deep architectural analysis',
48
48
  badge: 'Deep Dive',
49
49
  badgeClass: 'badge-power'
50
50
  }
@@ -156,16 +156,9 @@ class GeminiProvider extends AIProvider {
156
156
 
157
157
  if (this.useShell) {
158
158
  // In shell mode, build full command string with args
159
- // Quote the allowed-tools value to prevent shell interpretation of special characters
159
+ // Quote all args to prevent shell interpretation of special characters
160
160
  // (commas, parentheses in patterns like "run_shell_command(git diff)")
161
- const quotedBaseArgs = baseArgs.map((arg, i) => {
162
- // The allowed-tools value follows the --allowed-tools flag
163
- if (baseArgs[i - 1] === '--allowed-tools') {
164
- return `'${arg}'`;
165
- }
166
- return arg;
167
- });
168
- this.command = `${geminiCmd} ${[...quotedBaseArgs, ...providerArgs, ...modelArgs].join(' ')}`;
161
+ this.command = `${geminiCmd} ${quoteShellArgs([...baseArgs, ...providerArgs, ...modelArgs]).join(' ')}`;
169
162
  this.args = [];
170
163
  } else {
171
164
  this.command = geminiCmd;
@@ -616,7 +609,7 @@ class GeminiProvider extends AIProvider {
616
609
  // For extraction, we pass the prompt via stdin
617
610
  if (useShell) {
618
611
  return {
619
- command: `${geminiCmd} ${args.join(' ')}`,
612
+ command: `${geminiCmd} ${quoteShellArgs(args).join(' ')}`,
620
613
  args: [],
621
614
  useShell: true,
622
615
  promptViaStdin: true
@@ -13,7 +13,7 @@
13
13
 
14
14
  const path = require('path');
15
15
  const { spawn } = require('child_process');
16
- const { AIProvider, registerProvider } = require('./provider');
16
+ const { AIProvider, registerProvider, quoteShellArgs } = require('./provider');
17
17
  const logger = require('../utils/logger');
18
18
  const { extractJSON } = require('../utils/json-extractor');
19
19
  const { CancellationError, isAnalysisCancelled } = require('../routes/shared');
@@ -114,7 +114,7 @@ class OpenCodeProvider extends AIProvider {
114
114
  let fullArgs;
115
115
 
116
116
  if (this.useShell) {
117
- fullCommand = `${this.opencodeCmd} ${this.baseArgs.join(' ')}`;
117
+ fullCommand = `${this.opencodeCmd} ${quoteShellArgs(this.baseArgs).join(' ')}`;
118
118
  fullArgs = [];
119
119
  } else {
120
120
  fullCommand = this.opencodeCmd;
@@ -554,7 +554,7 @@ class OpenCodeProvider extends AIProvider {
554
554
  // OpenCode reads from stdin when no positional message arguments are provided
555
555
  if (useShell) {
556
556
  return {
557
- command: `${opencodeCmd} ${args.join(' ')}`,
557
+ command: `${opencodeCmd} ${quoteShellArgs(args).join(' ')}`,
558
558
  args: [],
559
559
  useShell: true,
560
560
  promptViaStdin: true
@@ -18,7 +18,7 @@
18
18
 
19
19
  const path = require('path');
20
20
  const { spawn } = require('child_process');
21
- const { AIProvider, registerProvider } = require('./provider');
21
+ const { AIProvider, registerProvider, quoteShellArgs } = require('./provider');
22
22
  const logger = require('../utils/logger');
23
23
  const { extractJSON } = require('../utils/json-extractor');
24
24
  const { CancellationError, isAnalysisCancelled } = require('../routes/shared');
@@ -253,7 +253,7 @@ class PiProvider extends AIProvider {
253
253
  let fullArgs;
254
254
 
255
255
  if (this.useShell) {
256
- fullCommand = `${this.piCmd} ${this.baseArgs.join(' ')}`;
256
+ fullCommand = `${this.piCmd} ${quoteShellArgs(this.baseArgs).join(' ')}`;
257
257
  fullArgs = [];
258
258
  } else {
259
259
  fullCommand = this.piCmd;
@@ -745,7 +745,7 @@ class PiProvider extends AIProvider {
745
745
  // Pi reads from stdin when using -p with no positional message arguments
746
746
  if (useShell) {
747
747
  return {
748
- command: `${piCmd} ${args.join(' ')}`,
748
+ command: `${piCmd} ${quoteShellArgs(args).join(' ')}`,
749
749
  args: [],
750
750
  useShell: true,
751
751
  promptViaStdin: true,
@@ -14,6 +14,24 @@ const { extractJSON } = require('../utils/json-extractor');
14
14
  // Directory containing bin scripts (git-diff-lines, etc.)
15
15
  const BIN_DIR = path.join(__dirname, '..', '..', 'bin');
16
16
 
17
+ /**
18
+ * Quote shell-sensitive arguments for safe shell execution.
19
+ * Any arg containing characters that could be interpreted by the shell
20
+ * (brackets, parentheses, commas, etc.) is wrapped in single quotes
21
+ * with internal single quotes escaped using the POSIX pattern.
22
+ *
23
+ * @param {string[]} args - Array of CLI arguments
24
+ * @returns {string[]} Args with shell-sensitive values quoted
25
+ */
26
+ function quoteShellArgs(args) {
27
+ return args.map(arg => {
28
+ if (/[[\]*?(){}$!&|;<>,\s'"\\`#~]/.test(arg)) {
29
+ return `'${arg.replace(/'/g, "'\\''")}'`;
30
+ }
31
+ return arg;
32
+ });
33
+ }
34
+
17
35
  /**
18
36
  * Model tier definitions - provider-agnostic tiers that map to specific models
19
37
  */
@@ -639,6 +657,7 @@ async function testProviderAvailability(providerId, timeout = 10000) {
639
657
  module.exports = {
640
658
  AIProvider,
641
659
  MODEL_TIERS,
660
+ quoteShellArgs,
642
661
  registerProvider,
643
662
  getProviderClass,
644
663
  getRegisteredProviderIds,