@in-the-loop-labs/pair-review 1.4.1 → 1.4.3
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 +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/js/index.js +26 -63
- package/public/js/local.js +1 -1
- package/public/js/modules/comment-manager.js +10 -5
- package/public/js/modules/diff-renderer.js +58 -0
- package/public/js/pr.js +17 -11
- package/src/ai/claude-provider.js +3 -25
- package/src/ai/codex-provider.js +13 -13
- package/src/ai/copilot-provider.js +42 -21
- package/src/ai/cursor-agent-provider.js +51 -15
- package/src/ai/gemini-provider.js +7 -14
- package/src/ai/opencode-provider.js +3 -3
- package/src/ai/pi-provider.js +3 -3
- package/src/ai/provider.js +19 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pair-review",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
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.
|
|
3
|
+
"version": "1.4.3",
|
|
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",
|
package/public/js/index.js
CHANGED
|
@@ -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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
//
|
|
885
|
-
|
|
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
|
})();
|
package/public/js/local.js
CHANGED
|
@@ -650,7 +650,7 @@ class LocalManager {
|
|
|
650
650
|
data-file="${fileName}"
|
|
651
651
|
data-line="${lineStart}"
|
|
652
652
|
data-line-end="${lineEnd}"
|
|
653
|
-
|
|
653
|
+
data-side="${side || 'RIGHT'}"
|
|
654
654
|
>${manager.escapeHtml(currentText)}</textarea>
|
|
655
655
|
<div class="comment-edit-actions">
|
|
656
656
|
<button class="btn btn-sm btn-primary save-edit-btn">Save</button>
|
|
@@ -245,12 +245,14 @@ class CommentManager {
|
|
|
245
245
|
const rows = targetWrapper.querySelectorAll('tr[data-line-number]');
|
|
246
246
|
const codeLines = [];
|
|
247
247
|
|
|
248
|
+
// Always filter by side to prevent including both OLD and NEW versions of modified lines.
|
|
249
|
+
// Default to 'RIGHT' because suggestions target the NEW version of code.
|
|
250
|
+
// This is the definitive fix: even if callers fail to propagate side, we never return both versions.
|
|
251
|
+
const effectiveSide = side || 'RIGHT';
|
|
252
|
+
|
|
248
253
|
for (const row of rows) {
|
|
249
254
|
const lineNum = parseInt(row.dataset.lineNumber, 10);
|
|
250
|
-
|
|
251
|
-
// Side filtering prevents including both deleted and added versions of modified lines
|
|
252
|
-
const matchesSide = !side || row.dataset.side === side;
|
|
253
|
-
if (lineNum >= startLine && lineNum <= endLine && row.dataset.fileName === fileName && matchesSide) {
|
|
255
|
+
if (lineNum >= startLine && lineNum <= endLine && row.dataset.fileName === fileName && row.dataset.side === effectiveSide) {
|
|
254
256
|
// Get the code content cell
|
|
255
257
|
const codeCell = row.querySelector('.d2h-code-line-ctn');
|
|
256
258
|
if (codeCell) {
|
|
@@ -279,6 +281,9 @@ class CommentManager {
|
|
|
279
281
|
const startLine = parseInt(textarea.dataset.line, 10);
|
|
280
282
|
const endLine = parseInt(textarea.dataset.lineEnd, 10) || startLine;
|
|
281
283
|
const side = textarea.dataset.side;
|
|
284
|
+
if (!side) {
|
|
285
|
+
console.warn('[Suggestion] textarea missing data-side attribute, defaulting to RIGHT');
|
|
286
|
+
}
|
|
282
287
|
|
|
283
288
|
// Get the code from the selected lines (pass side to avoid including both deleted and added lines)
|
|
284
289
|
const code = this.getCodeFromLines(fileName, startLine, endLine, side);
|
|
@@ -587,7 +592,7 @@ class CommentManager {
|
|
|
587
592
|
data-file="${comment.file}"
|
|
588
593
|
data-line="${comment.line_start}"
|
|
589
594
|
data-line-end="${comment.line_end || comment.line_start}"
|
|
590
|
-
|
|
595
|
+
data-side="${comment.side || 'RIGHT'}"
|
|
591
596
|
>${escapeHtml(comment.body)}</textarea>
|
|
592
597
|
<div class="comment-edit-actions">
|
|
593
598
|
<button class="btn btn-sm btn-primary save-edit-btn">
|
|
@@ -517,6 +517,8 @@ class DiffRenderer {
|
|
|
517
517
|
|
|
518
518
|
const headerContent = functionContext
|
|
519
519
|
? `<span class="hunk-context-icon" aria-label="Function context">f</span><span class="hunk-context-text">${DiffRenderer.escapeHtml(functionContext)}</span>`
|
|
520
|
+
// "..." dividers act as visual separators between non-contiguous code
|
|
521
|
+
// sections when git couldn't identify a surrounding function/class scope.
|
|
520
522
|
: '<span class="hunk-divider" aria-label="Code section divider">...</span>';
|
|
521
523
|
|
|
522
524
|
headerRow.innerHTML = `<td colspan="2" class="d2h-info">${headerContent}</td>`;
|
|
@@ -554,6 +556,62 @@ class DiffRenderer {
|
|
|
554
556
|
trimmedLine.includes(trimmedContext + ' ');
|
|
555
557
|
}
|
|
556
558
|
|
|
559
|
+
/**
|
|
560
|
+
* Handle hunk header rows that are no longer at a gap boundary.
|
|
561
|
+
* A hunk header (d2h-info row) should only be visible when:
|
|
562
|
+
* 1. It is the first row in the tbody (file-level context), OR
|
|
563
|
+
* 2. Its immediately preceding sibling is a gap row (context-expand-row)
|
|
564
|
+
*
|
|
565
|
+
* When a header with function context (_f_ marker) becomes stranded after
|
|
566
|
+
* upward expansion, it is relocated to the nearest gap row above within the
|
|
567
|
+
* same hunk section. This preserves the function context indicator for the
|
|
568
|
+
* remaining gap, whether or not the function definition is now visible below.
|
|
569
|
+
*
|
|
570
|
+
* Headers without function context (... dividers) or those with no reachable
|
|
571
|
+
* gap above are removed.
|
|
572
|
+
* @param {HTMLElement} tbody - The table body containing the diff
|
|
573
|
+
*/
|
|
574
|
+
static removeStrandedHunkHeaders(tbody) {
|
|
575
|
+
if (!tbody) return;
|
|
576
|
+
|
|
577
|
+
const infoRows = tbody.querySelectorAll('tr.d2h-info');
|
|
578
|
+
for (const row of infoRows) {
|
|
579
|
+
const prev = row.previousElementSibling;
|
|
580
|
+
// Keep if this is the first row in the tbody (provides file-level context)
|
|
581
|
+
if (!prev) continue;
|
|
582
|
+
// Keep if preceded by a gap row (the header marks the boundary)
|
|
583
|
+
if (prev.classList.contains('context-expand-row')) continue;
|
|
584
|
+
|
|
585
|
+
// Header is stranded between visible code lines.
|
|
586
|
+
// If it has function context, try to relocate it to the nearest gap above
|
|
587
|
+
// so the gap retains its function context indicator.
|
|
588
|
+
if (row.dataset.functionContext) {
|
|
589
|
+
let sibling = row.previousElementSibling;
|
|
590
|
+
let nearestGap = null;
|
|
591
|
+
while (sibling) {
|
|
592
|
+
if (sibling.classList.contains('context-expand-row')) {
|
|
593
|
+
nearestGap = sibling;
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
// Stop at another hunk header - don't cross hunk boundaries.
|
|
597
|
+
// (This also means only the nearest stranded header in DOM order
|
|
598
|
+
// gets relocated to a given gap; subsequent ones are removed.)
|
|
599
|
+
if (sibling.classList.contains('d2h-info')) break;
|
|
600
|
+
sibling = sibling.previousElementSibling;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (nearestGap) {
|
|
604
|
+
// Relocate: move the header to right after the gap row
|
|
605
|
+
nearestGap.insertAdjacentElement('afterend', row);
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// No gap found above or no function context - remove the stranded header
|
|
611
|
+
row.remove();
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
557
615
|
/**
|
|
558
616
|
* Update function context visibility for all hunk headers in a file's tbody
|
|
559
617
|
* Called once after any expansion in a file - checks all headers efficiently
|
package/public/js/pr.js
CHANGED
|
@@ -1455,9 +1455,10 @@ class PRManager {
|
|
|
1455
1455
|
gapRow.parentNode.insertBefore(fragment, gapRow);
|
|
1456
1456
|
gapRow.remove();
|
|
1457
1457
|
|
|
1458
|
-
//
|
|
1459
|
-
//
|
|
1458
|
+
// Remove hunk headers that are no longer at a gap boundary,
|
|
1459
|
+
// then check remaining headers for visible function definitions
|
|
1460
1460
|
if (window.DiffRenderer) {
|
|
1461
|
+
window.DiffRenderer.removeStrandedHunkHeaders(tbody);
|
|
1461
1462
|
window.DiffRenderer.updateFunctionContextVisibility(tbody);
|
|
1462
1463
|
}
|
|
1463
1464
|
|
|
@@ -1478,6 +1479,7 @@ class PRManager {
|
|
|
1478
1479
|
const hasExplicitEndLineNew = !isNaN(parseInt(controls.dataset.endLineNew));
|
|
1479
1480
|
|
|
1480
1481
|
const fileName = controls.dataset.fileName;
|
|
1482
|
+
const position = controls.dataset.position || 'between';
|
|
1481
1483
|
const tbody = gapRow.closest('tbody');
|
|
1482
1484
|
|
|
1483
1485
|
if (!tbody) return;
|
|
@@ -1488,6 +1490,13 @@ class PRManager {
|
|
|
1488
1490
|
|
|
1489
1491
|
const fragment = document.createDocumentFragment();
|
|
1490
1492
|
|
|
1493
|
+
// Compute positions for each remnant based on file boundary proximity.
|
|
1494
|
+
// The upper remnant keeps 'above' only if the original gap was at the file start;
|
|
1495
|
+
// the lower remnant keeps 'below' only if the original gap was at the file end.
|
|
1496
|
+
// Inner remnants become 'between' since they're sandwiched between visible content.
|
|
1497
|
+
const gapAbovePosition = position === 'above' ? 'above' : 'between';
|
|
1498
|
+
const gapBelowPosition = position === 'below' ? 'below' : 'between';
|
|
1499
|
+
|
|
1491
1500
|
// Create gap above if needed
|
|
1492
1501
|
const gapAboveSize = expandStart - gapStart;
|
|
1493
1502
|
if (gapAboveSize > 0) {
|
|
@@ -1496,7 +1505,7 @@ class PRManager {
|
|
|
1496
1505
|
gapStart,
|
|
1497
1506
|
expandStart - 1,
|
|
1498
1507
|
gapAboveSize,
|
|
1499
|
-
|
|
1508
|
+
gapAbovePosition,
|
|
1500
1509
|
(controls, dir, cnt) => this.expandGapContext(controls, dir, cnt),
|
|
1501
1510
|
gapStartNew // Preserve the NEW line number offset
|
|
1502
1511
|
);
|
|
@@ -1537,7 +1546,7 @@ class PRManager {
|
|
|
1537
1546
|
expandEnd + 1,
|
|
1538
1547
|
gapEnd,
|
|
1539
1548
|
gapBelowSize,
|
|
1540
|
-
|
|
1549
|
+
gapBelowPosition,
|
|
1541
1550
|
(controls, dir, cnt) => this.expandGapContext(controls, dir, cnt),
|
|
1542
1551
|
belowGapStartNew // Updated NEW line number for gap below
|
|
1543
1552
|
);
|
|
@@ -1553,9 +1562,10 @@ class PRManager {
|
|
|
1553
1562
|
gapRow.parentNode.insertBefore(fragment, gapRow);
|
|
1554
1563
|
gapRow.remove();
|
|
1555
1564
|
|
|
1556
|
-
//
|
|
1557
|
-
//
|
|
1565
|
+
// Remove hunk headers that are no longer at a gap boundary,
|
|
1566
|
+
// then check remaining headers for visible function definitions
|
|
1558
1567
|
if (window.DiffRenderer) {
|
|
1568
|
+
window.DiffRenderer.removeStrandedHunkHeaders(tbody);
|
|
1559
1569
|
window.DiffRenderer.updateFunctionContextVisibility(tbody);
|
|
1560
1570
|
}
|
|
1561
1571
|
|
|
@@ -1718,10 +1728,6 @@ class PRManager {
|
|
|
1718
1728
|
this.commentManager.updateSuggestionButtonState(textarea, button);
|
|
1719
1729
|
}
|
|
1720
1730
|
|
|
1721
|
-
getCodeFromLines(fileName, startLine, endLine) {
|
|
1722
|
-
return this.commentManager.getCodeFromLines(fileName, startLine, endLine);
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1725
1731
|
insertSuggestionBlock(textarea, button) {
|
|
1726
1732
|
this.commentManager.insertSuggestionBlock(textarea, button);
|
|
1727
1733
|
}
|
|
@@ -1783,7 +1789,7 @@ class PRManager {
|
|
|
1783
1789
|
data-file="${fileName}"
|
|
1784
1790
|
data-line="${lineStart}"
|
|
1785
1791
|
data-line-end="${lineEnd}"
|
|
1786
|
-
|
|
1792
|
+
data-side="${side || 'RIGHT'}"
|
|
1787
1793
|
>${this.escapeHtml(currentText)}</textarea>
|
|
1788
1794
|
<div class="comment-edit-actions">
|
|
1789
1795
|
<button class="btn btn-sm btn-primary save-edit-btn">Save</button>
|
|
@@ -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} ${
|
|
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 =
|
|
467
|
+
const quotedArgs = quoteShellArgs(args);
|
|
490
468
|
return {
|
|
491
469
|
command: `${claudeCmd} ${quotedArgs.join(' ')}`,
|
|
492
470
|
args: [],
|
package/src/ai/codex-provider.js
CHANGED
|
@@ -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.
|
|
26
|
-
* - gpt-5.
|
|
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.
|
|
40
|
-
name: 'GPT-5.
|
|
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—
|
|
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.
|
|
50
|
-
name: 'GPT-5.
|
|
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
|
|
53
|
+
description: 'Most capable agentic coding model—combines 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.
|
|
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.
|
|
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: '
|
|
28
|
-
name: '
|
|
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
|
|
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: '
|
|
37
|
-
name: '
|
|
39
|
+
id: 'claude-sonnet-4.5',
|
|
40
|
+
name: 'Claude Sonnet 4.5',
|
|
38
41
|
tier: 'balanced',
|
|
39
42
|
tagline: 'Reliable Review',
|
|
40
|
-
description: '
|
|
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: '
|
|
47
|
-
name: '
|
|
48
|
-
tier: '
|
|
49
|
-
tagline: '
|
|
50
|
-
description: '
|
|
51
|
-
badge: '
|
|
52
|
-
badgeClass: 'badge-
|
|
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: '
|
|
58
|
-
tagline: '
|
|
59
|
-
description: '
|
|
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 = '
|
|
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 '
|
|
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.
|
|
34
|
-
* - balanced (sonnet-4.5-thinking, gemini-3-pro): Recommended for most reviews
|
|
35
|
-
* - thorough (gpt-5.
|
|
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: '
|
|
49
|
-
name: '
|
|
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: '
|
|
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.
|
|
77
|
-
name: 'GPT-5.
|
|
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
|
|
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
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
|
159
|
+
// Quote all args to prevent shell interpretation of special characters
|
|
160
160
|
// (commas, parentheses in patterns like "run_shell_command(git diff)")
|
|
161
|
-
|
|
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
|
package/src/ai/pi-provider.js
CHANGED
|
@@ -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,
|
package/src/ai/provider.js
CHANGED
|
@@ -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,
|