@in-the-loop-labs/pair-review 1.4.0 → 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 +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/index.html +6 -1
- package/public/js/index.js +26 -63
- package/public/js/local.js +60 -30
- package/public/js/pr.js +57 -38
- 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.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.
|
|
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",
|
package/public/index.html
CHANGED
|
@@ -1176,7 +1176,12 @@
|
|
|
1176
1176
|
"port": 7247,
|
|
1177
1177
|
"theme": "light"
|
|
1178
1178
|
}</code></pre>
|
|
1179
|
-
<p>
|
|
1179
|
+
<p>Create a <a href="https://github.com/settings/tokens/new" target="_blank" rel="noopener" style="color: var(--color-text-link);">GitHub Personal Access Token</a> with the following scopes:</p>
|
|
1180
|
+
<ul style="margin: 0.5em 0; padding-left: 1.5em;">
|
|
1181
|
+
<li><code>repo</code> — required for private repositories</li>
|
|
1182
|
+
<li><code>public_repo</code> — sufficient for public repositories only</li>
|
|
1183
|
+
</ul>
|
|
1184
|
+
<p style="margin-top: 0.5em;">You can also provide the token via the <code>GITHUB_TOKEN</code> environment variable, which takes precedence over the config file.</p>
|
|
1180
1185
|
</div>
|
|
1181
1186
|
</div>
|
|
1182
1187
|
</div>
|
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
|
@@ -304,14 +304,62 @@ class LocalManager {
|
|
|
304
304
|
return;
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
//
|
|
307
|
+
// Run stale check and settings fetch in parallel to minimize dialog delay.
|
|
308
|
+
// Use AbortController so the fetch is truly cancelled on timeout,
|
|
309
|
+
// freeing the HTTP connection for subsequent requests.
|
|
310
|
+
const STALE_TIMEOUT = 2000;
|
|
311
|
+
const staleAbort = new AbortController();
|
|
312
|
+
const staleTimer = setTimeout(() => {
|
|
313
|
+
console.debug(`[Analyze] stale-check timed out after ${STALE_TIMEOUT}ms, aborting`);
|
|
314
|
+
staleAbort.abort();
|
|
315
|
+
}, STALE_TIMEOUT);
|
|
316
|
+
const staleCheckPromise = fetch(`/api/local/${reviewId}/check-stale`, { signal: staleAbort.signal })
|
|
317
|
+
.then(r => {
|
|
318
|
+
clearTimeout(staleTimer);
|
|
319
|
+
if (!r.ok) {
|
|
320
|
+
console.warn(`Stale check failed with status ${r.status}`);
|
|
321
|
+
return { _fetchError: true, status: r.status };
|
|
322
|
+
}
|
|
323
|
+
return r.json();
|
|
324
|
+
})
|
|
325
|
+
.catch(err => {
|
|
326
|
+
clearTimeout(staleTimer);
|
|
327
|
+
if (err.name === 'AbortError') {
|
|
328
|
+
console.debug('[Analyze] stale-check aborted (timeout)');
|
|
329
|
+
} else {
|
|
330
|
+
console.warn('[Analyze] stale-check fetch error:', err);
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
});
|
|
334
|
+
|
|
308
335
|
try {
|
|
309
|
-
|
|
310
|
-
if (
|
|
311
|
-
|
|
336
|
+
// Show analysis config modal
|
|
337
|
+
if (!manager.analysisConfigModal) {
|
|
338
|
+
clearTimeout(staleTimer);
|
|
339
|
+
console.warn('AnalysisConfigModal not initialized, proceeding without config');
|
|
340
|
+
await self.startLocalAnalysis(btn, {});
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
312
343
|
|
|
313
|
-
|
|
314
|
-
|
|
344
|
+
// Fetch stale check, repo settings, and last instructions in parallel
|
|
345
|
+
const [staleResult, repoSettings, lastInstructions] = await Promise.all([
|
|
346
|
+
staleCheckPromise,
|
|
347
|
+
manager.fetchRepoSettings().catch(() => null),
|
|
348
|
+
manager.fetchLastCustomInstructions().catch(() => '')
|
|
349
|
+
]);
|
|
350
|
+
|
|
351
|
+
// Process stale check result
|
|
352
|
+
try {
|
|
353
|
+
if (staleResult === null) {
|
|
354
|
+
// Timed out or network error — fail open
|
|
355
|
+
if (window.toast) {
|
|
356
|
+
window.toast.showWarning('Could not verify working directory is current.');
|
|
357
|
+
}
|
|
358
|
+
} else if (staleResult._fetchError) {
|
|
359
|
+
if (window.toast) {
|
|
360
|
+
window.toast.showWarning(`Could not verify working directory is current (${staleResult.status}).`);
|
|
361
|
+
}
|
|
362
|
+
} else if (staleResult.isStale === true) {
|
|
315
363
|
if (window.confirmDialog) {
|
|
316
364
|
const choice = await window.confirmDialog.show({
|
|
317
365
|
title: 'Files Have Changed',
|
|
@@ -323,40 +371,22 @@ class LocalManager {
|
|
|
323
371
|
});
|
|
324
372
|
|
|
325
373
|
if (choice === 'confirm') {
|
|
326
|
-
// User wants to refresh first, then continue to analysis
|
|
327
374
|
await self.refreshDiff();
|
|
328
|
-
// Continue to config modal after refresh (don't return)
|
|
329
375
|
} else if (choice !== 'secondary') {
|
|
330
|
-
// User cancelled
|
|
331
376
|
return;
|
|
332
377
|
}
|
|
333
|
-
// Both 'confirm' (after refresh) and 'secondary' continue to config modal
|
|
334
378
|
}
|
|
335
|
-
} else if (
|
|
336
|
-
// Couldn't verify - show toast warning
|
|
379
|
+
} else if (staleResult.isStale === null && staleResult.error) {
|
|
337
380
|
if (window.toast) {
|
|
338
381
|
window.toast.showWarning('Could not verify working directory is current.');
|
|
339
382
|
}
|
|
340
383
|
}
|
|
384
|
+
} catch (staleError) {
|
|
385
|
+
console.warn('[Local] Error processing staleness:', staleError);
|
|
386
|
+
if (window.toast) {
|
|
387
|
+
window.toast.showWarning('Could not verify working directory is current.');
|
|
388
|
+
}
|
|
341
389
|
}
|
|
342
|
-
} catch (staleError) {
|
|
343
|
-
console.warn('[Local] Error checking staleness:', staleError);
|
|
344
|
-
if (window.toast) {
|
|
345
|
-
window.toast.showWarning('Could not verify working directory is current.');
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
try {
|
|
350
|
-
// Show analysis config modal
|
|
351
|
-
if (!manager.analysisConfigModal) {
|
|
352
|
-
console.warn('AnalysisConfigModal not initialized, proceeding without config');
|
|
353
|
-
await self.startLocalAnalysis(btn, {});
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Get repo settings for default instructions
|
|
358
|
-
const repoSettings = await manager.fetchRepoSettings().catch(() => null);
|
|
359
|
-
const lastInstructions = await manager.fetchLastCustomInstructions().catch(() => '');
|
|
360
390
|
|
|
361
391
|
// Determine model and provider
|
|
362
392
|
const modelStorageKey = `pair-review-model:local-${reviewId}`;
|
package/public/js/pr.js
CHANGED
|
@@ -3432,37 +3432,74 @@ class PRManager {
|
|
|
3432
3432
|
}
|
|
3433
3433
|
|
|
3434
3434
|
try {
|
|
3435
|
-
//
|
|
3435
|
+
// Run stale check and settings fetch in parallel to minimize dialog delay.
|
|
3436
|
+
// Use AbortController so the fetch is truly cancelled on timeout,
|
|
3437
|
+
// freeing the HTTP connection for subsequent requests.
|
|
3438
|
+
const STALE_TIMEOUT = 2000;
|
|
3439
|
+
const staleAbort = new AbortController();
|
|
3440
|
+
const staleTimer = setTimeout(() => {
|
|
3441
|
+
console.debug(`[Analyze] stale-check timed out after ${STALE_TIMEOUT}ms, aborting`);
|
|
3442
|
+
staleAbort.abort();
|
|
3443
|
+
}, STALE_TIMEOUT);
|
|
3444
|
+
const staleCheckPromise = fetch(`/api/pr/${owner}/${repo}/${number}/check-stale`, { signal: staleAbort.signal })
|
|
3445
|
+
.then(r => {
|
|
3446
|
+
clearTimeout(staleTimer);
|
|
3447
|
+
if (!r.ok) {
|
|
3448
|
+
console.warn(`Stale check failed with status ${r.status}`);
|
|
3449
|
+
return { _fetchError: true, status: r.status };
|
|
3450
|
+
}
|
|
3451
|
+
return r.json();
|
|
3452
|
+
})
|
|
3453
|
+
.catch(err => {
|
|
3454
|
+
clearTimeout(staleTimer);
|
|
3455
|
+
if (err.name === 'AbortError') {
|
|
3456
|
+
console.debug('[Analyze] stale-check aborted (timeout)');
|
|
3457
|
+
} else {
|
|
3458
|
+
console.warn('[Analyze] stale-check fetch error:', err);
|
|
3459
|
+
}
|
|
3460
|
+
return null;
|
|
3461
|
+
});
|
|
3462
|
+
|
|
3463
|
+
// Show analysis config modal
|
|
3464
|
+
if (!this.analysisConfigModal) {
|
|
3465
|
+
clearTimeout(staleTimer);
|
|
3466
|
+
console.warn('AnalysisConfigModal not initialized, proceeding without config');
|
|
3467
|
+
await this.startAnalysis(owner, repo, number, btn, {});
|
|
3468
|
+
return;
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
// Fetch stale check, repo settings, and last instructions in parallel
|
|
3472
|
+
const [staleResult, repoSettings, lastInstructions] = await Promise.all([
|
|
3473
|
+
staleCheckPromise,
|
|
3474
|
+
this.fetchRepoSettings().catch(() => null),
|
|
3475
|
+
this.fetchLastCustomInstructions().catch(() => '')
|
|
3476
|
+
]);
|
|
3477
|
+
|
|
3478
|
+
// Process stale check result
|
|
3436
3479
|
try {
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3480
|
+
if (staleResult === null) {
|
|
3481
|
+
// Timed out or network error — fail open
|
|
3482
|
+
if (window.toast) {
|
|
3483
|
+
window.toast.showWarning('Could not verify PR is current. Proceeding with analysis.');
|
|
3484
|
+
}
|
|
3485
|
+
} else if (staleResult._fetchError) {
|
|
3442
3486
|
if (window.toast) {
|
|
3443
|
-
window.toast.showWarning(`Could not verify PR is current (${
|
|
3487
|
+
window.toast.showWarning(`Could not verify PR is current (${staleResult.status}). Proceeding with analysis.`);
|
|
3444
3488
|
}
|
|
3445
|
-
// Fall through to continue with analysis
|
|
3446
3489
|
} else {
|
|
3447
|
-
const staleData = await staleResponse.json();
|
|
3448
|
-
|
|
3449
3490
|
// Handle PR state - show info for closed/merged PRs but still allow analysis
|
|
3450
|
-
if (
|
|
3451
|
-
const stateLabel =
|
|
3491
|
+
if (staleResult.prState && (staleResult.prState !== 'open' || staleResult.merged)) {
|
|
3492
|
+
const stateLabel = staleResult.merged ? 'merged' : 'closed';
|
|
3452
3493
|
if (window.toast) {
|
|
3453
3494
|
window.toast.showWarning(`This PR is ${stateLabel}. Analysis will proceed on the existing data.`);
|
|
3454
3495
|
}
|
|
3455
3496
|
}
|
|
3456
3497
|
|
|
3457
|
-
|
|
3458
|
-
if (staleData.isStale === null) {
|
|
3459
|
-
// Couldn't verify - show toast and proceed
|
|
3498
|
+
if (staleResult.isStale === null) {
|
|
3460
3499
|
if (window.toast) {
|
|
3461
3500
|
window.toast.showWarning('Could not verify PR is current. Proceeding with analysis.');
|
|
3462
3501
|
}
|
|
3463
|
-
|
|
3464
|
-
} else if (staleData.isStale === true) {
|
|
3465
|
-
// PR is stale - show single dialog with 3 options
|
|
3502
|
+
} else if (staleResult.isStale === true) {
|
|
3466
3503
|
if (!window.confirmDialog) {
|
|
3467
3504
|
console.warn('ConfirmDialog not available for stale PR check');
|
|
3468
3505
|
} else {
|
|
@@ -3476,40 +3513,22 @@ class PRManager {
|
|
|
3476
3513
|
});
|
|
3477
3514
|
|
|
3478
3515
|
if (choice === 'confirm') {
|
|
3479
|
-
// User wants to refresh first
|
|
3480
3516
|
await this.refreshPR();
|
|
3481
|
-
// After refresh, continue with analysis
|
|
3482
3517
|
} else if (choice === 'secondary') {
|
|
3483
|
-
// User chose to analyze anyway
|
|
3518
|
+
// User chose to analyze anyway
|
|
3484
3519
|
} else {
|
|
3485
|
-
// User cancelled
|
|
3486
3520
|
return;
|
|
3487
3521
|
}
|
|
3488
3522
|
}
|
|
3489
3523
|
}
|
|
3490
|
-
// If isStale === false, PR is up-to-date, just continue
|
|
3491
3524
|
}
|
|
3492
3525
|
} catch (staleError) {
|
|
3493
|
-
|
|
3494
|
-
console.warn('Error checking PR staleness:', staleError);
|
|
3526
|
+
console.warn('Error processing PR staleness:', staleError);
|
|
3495
3527
|
if (window.toast) {
|
|
3496
3528
|
window.toast.showWarning('Could not verify PR is current. Proceeding with analysis.');
|
|
3497
3529
|
}
|
|
3498
3530
|
}
|
|
3499
3531
|
|
|
3500
|
-
// Show analysis config modal
|
|
3501
|
-
if (!this.analysisConfigModal) {
|
|
3502
|
-
console.warn('AnalysisConfigModal not initialized, proceeding without config');
|
|
3503
|
-
await this.startAnalysis(owner, repo, number, btn, {});
|
|
3504
|
-
return;
|
|
3505
|
-
}
|
|
3506
|
-
|
|
3507
|
-
// Fetch repo settings and last used instructions in parallel
|
|
3508
|
-
const [repoSettings, lastInstructions] = await Promise.all([
|
|
3509
|
-
this.fetchRepoSettings(),
|
|
3510
|
-
this.fetchLastCustomInstructions()
|
|
3511
|
-
]);
|
|
3512
|
-
|
|
3513
3532
|
// Determine the model and provider to use (priority: remembered > repo default > defaults)
|
|
3514
3533
|
const modelStorageKey = PRManager.getRepoStorageKey('pair-review-model', owner, repo);
|
|
3515
3534
|
const providerStorageKey = PRManager.getRepoStorageKey('pair-review-provider', owner, repo);
|
|
@@ -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,
|