@in-the-loop-labs/pair-review 3.7.2 → 3.9.0

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.
@@ -763,9 +763,10 @@ class CodexProvider extends AIProvider {
763
763
  * Test if Codex CLI is available
764
764
  * Uses fast `--version` check instead of running a prompt.
765
765
  * Uses the command configured in the instance (respects ENV > config > default precedence)
766
+ * @param {number} [timeoutMs=10000] - Timeout in milliseconds for the probe
766
767
  * @returns {Promise<boolean>}
767
768
  */
768
- async testAvailability() {
769
+ async testAvailability(timeoutMs = 10000) {
769
770
  return new Promise((resolve) => {
770
771
  // For availability test, we just need to check --version
771
772
  // Use the already-resolved command from the constructor (this.codexCmd)
@@ -794,10 +795,10 @@ class CodexProvider extends AIProvider {
794
795
  const availabilityTimeout = setTimeout(() => {
795
796
  if (settled) return;
796
797
  settled = true;
797
- logger.warn('Codex CLI availability check timed out after 10s');
798
+ logger.warn(`Codex CLI availability check timed out after ${Math.round(timeoutMs / 1000)}s`);
798
799
  try { codex.kill(); } catch { /* ignore */ }
799
800
  resolve(false);
800
- }, 10000);
801
+ }, timeoutMs);
801
802
 
802
803
  codex.stdout.on('data', (data) => {
803
804
  stdout += data.toString();
@@ -439,9 +439,11 @@ class CopilotProvider extends AIProvider {
439
439
  /**
440
440
  * Test if Copilot CLI is available
441
441
  * Uses the command configured in the instance (respects ENV > config > default precedence)
442
+ * @param {number} [timeoutMs=10000] - Timeout in milliseconds for the probe.
443
+ * Production passes the per-provider resolved value; the default is only hit by tests.
442
444
  * @returns {Promise<boolean>}
443
445
  */
444
- async testAvailability() {
446
+ async testAvailability(timeoutMs = 10000) {
445
447
  return new Promise((resolve) => {
446
448
  // For availability test, check --version
447
449
  // Use the already-resolved command from the constructor (this.copilotCmd)
@@ -465,6 +467,16 @@ class CopilotProvider extends AIProvider {
465
467
  let stdout = '';
466
468
  let settled = false;
467
469
 
470
+ // Timeout guard: if the CLI hangs, kill it and resolve false so the probe
471
+ // does not leak a child process.
472
+ const availabilityTimeout = setTimeout(() => {
473
+ if (settled) return;
474
+ settled = true;
475
+ logger.warn(`Copilot CLI availability check timed out after ${Math.round(timeoutMs / 1000)}s`);
476
+ try { copilot.kill(); } catch { /* ignore */ }
477
+ resolve(false);
478
+ }, timeoutMs);
479
+
468
480
  copilot.stdout.on('data', (data) => {
469
481
  stdout += data.toString();
470
482
  });
@@ -472,6 +484,7 @@ class CopilotProvider extends AIProvider {
472
484
  copilot.on('close', (code) => {
473
485
  if (settled) return;
474
486
  settled = true;
487
+ clearTimeout(availabilityTimeout);
475
488
  // Copilot CLI typically outputs version info on success
476
489
  if (code === 0) {
477
490
  logger.info(`Copilot CLI available: ${stdout.trim()}`);
@@ -485,6 +498,7 @@ class CopilotProvider extends AIProvider {
485
498
  copilot.on('error', (error) => {
486
499
  if (settled) return;
487
500
  settled = true;
501
+ clearTimeout(availabilityTimeout);
488
502
  logger.warn(`Copilot CLI not available: ${error.message}`);
489
503
  resolve(false);
490
504
  });
@@ -783,9 +783,10 @@ class CursorAgentProvider extends AIProvider {
783
783
  /**
784
784
  * Test if Cursor Agent CLI is available
785
785
  * Uses the command configured in the instance (respects ENV > config > default precedence)
786
+ * @param {number} [timeoutMs=10000] - Timeout in milliseconds for the probe
786
787
  * @returns {Promise<boolean>}
787
788
  */
788
- async testAvailability() {
789
+ async testAvailability(timeoutMs = 10000) {
789
790
  return new Promise((resolve) => {
790
791
  // For availability test, we just need to check --version
791
792
  // Use the already-resolved command from the constructor (this.agentCmd)
@@ -813,10 +814,10 @@ class CursorAgentProvider extends AIProvider {
813
814
  const availabilityTimeout = setTimeout(() => {
814
815
  if (settled) return;
815
816
  settled = true;
816
- logger.warn('Cursor Agent CLI availability check timed out after 10s');
817
+ logger.warn(`Cursor Agent CLI availability check timed out after ${Math.round(timeoutMs / 1000)}s`);
817
818
  try { agent.kill(); } catch { /* ignore */ }
818
819
  resolve(false);
819
- }, 10000);
820
+ }, timeoutMs);
820
821
 
821
822
  agent.stdout.on('data', (data) => {
822
823
  stdout += data.toString();
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  const providerModule = require('./provider');
15
- const { AIProvider, resolveDefaultModel, inferModelDefaults } = providerModule;
15
+ const { AIProvider, resolveDefaultModel, inferModelDefaults, secondsToTimeoutMs } = providerModule;
16
16
  const { spawn } = require('child_process');
17
17
  const { glob } = require('glob');
18
18
  const fs = require('fs').promises;
@@ -136,6 +136,10 @@ function createExecutableProviderClass(id, config) {
136
136
  this.mappingInstructions = config.mapping_instructions || '';
137
137
  this.timeout = config.timeout || 600000; // Default 10 minutes
138
138
  this.availabilityCommand = config.availability_command || 'true';
139
+ // Availability-probe timeout. Configured in seconds (mirrors
140
+ // checkout_timeout_seconds); falls back to the shared default when
141
+ // unset/invalid.
142
+ this.availabilityTimeoutMs = secondsToTimeoutMs(config.availability_timeout_seconds);
139
143
  this.extraEnv = {
140
144
  ...(config.env || {}),
141
145
  ...(configOverrides.env || {}),
@@ -454,9 +458,13 @@ function createExecutableProviderClass(id, config) {
454
458
  * Test if the external tool is available.
455
459
  * Runs the configured availability_command (defaults to 'true', i.e. always available).
456
460
  *
461
+ * @param {number} [timeoutMs] - Timeout in milliseconds for the probe.
462
+ * Defaults to the provider's configured availability_timeout_seconds
463
+ * (or 10s). Tools with slow build-based availability commands can raise
464
+ * this via `availability_timeout_seconds` in their provider config.
457
465
  * @returns {Promise<boolean>}
458
466
  */
459
- async testAvailability() {
467
+ async testAvailability(timeoutMs = this.availabilityTimeoutMs) {
460
468
  return new Promise((resolve) => {
461
469
  const command = this.availabilityCommand;
462
470
  logger.debug(`${id} availability check: ${command}`);
@@ -471,10 +479,10 @@ function createExecutableProviderClass(id, config) {
471
479
  const availabilityTimeout = setTimeout(() => {
472
480
  if (settled) return;
473
481
  settled = true;
474
- logger.warn(`${id} availability check timed out after 10s`);
482
+ logger.warn(`${id} availability check timed out after ${Math.round(timeoutMs / 1000)}s`);
475
483
  try { child.kill(); } catch { /* ignore */ }
476
484
  resolve(false);
477
- }, 10000);
485
+ }, timeoutMs);
478
486
 
479
487
  child.on('close', (code) => {
480
488
  if (settled) return;
@@ -659,9 +659,11 @@ class GeminiProvider extends AIProvider {
659
659
  /**
660
660
  * Test if Gemini CLI is available
661
661
  * Uses the command configured in the instance (respects ENV > config > default precedence)
662
+ * @param {number} [timeoutMs=10000] - Timeout in milliseconds for the probe.
663
+ * Production passes the per-provider resolved value; the default is only hit by tests.
662
664
  * @returns {Promise<boolean>}
663
665
  */
664
- async testAvailability() {
666
+ async testAvailability(timeoutMs = 10000) {
665
667
  return new Promise((resolve) => {
666
668
  // For availability test, we just need to check --version
667
669
  // Use the already-resolved command from the constructor (this.geminiCmd)
@@ -685,6 +687,16 @@ class GeminiProvider extends AIProvider {
685
687
  let stdout = '';
686
688
  let settled = false;
687
689
 
690
+ // Timeout guard: if the CLI hangs, kill it and resolve false so the probe
691
+ // does not leak a child process.
692
+ const availabilityTimeout = setTimeout(() => {
693
+ if (settled) return;
694
+ settled = true;
695
+ logger.warn(`Gemini CLI availability check timed out after ${Math.round(timeoutMs / 1000)}s`);
696
+ try { gemini.kill(); } catch { /* ignore */ }
697
+ resolve(false);
698
+ }, timeoutMs);
699
+
688
700
  gemini.stdout.on('data', (data) => {
689
701
  stdout += data.toString();
690
702
  });
@@ -692,6 +704,7 @@ class GeminiProvider extends AIProvider {
692
704
  gemini.on('close', (code) => {
693
705
  if (settled) return;
694
706
  settled = true;
707
+ clearTimeout(availabilityTimeout);
695
708
  if (code === 0 && stdout.includes('.')) {
696
709
  logger.info(`Gemini CLI available: ${stdout.trim()}`);
697
710
  resolve(true);
@@ -704,6 +717,7 @@ class GeminiProvider extends AIProvider {
704
717
  gemini.on('error', (error) => {
705
718
  if (settled) return;
706
719
  settled = true;
720
+ clearTimeout(availabilityTimeout);
707
721
  logger.warn(`Gemini CLI not available: ${error.message}`);
708
722
  resolve(false);
709
723
  });
package/src/ai/index.js CHANGED
@@ -17,10 +17,17 @@ const {
17
17
  getAllProvidersInfo,
18
18
  createProvider,
19
19
  testProviderAvailability,
20
+ resolveAvailabilityTimeoutMs,
21
+ secondsToTimeoutMs,
22
+ DEFAULT_AVAILABILITY_TIMEOUT_MS,
20
23
  applyConfigOverrides,
21
24
  getProviderConfigOverrides,
22
25
  inferModelDefaults,
23
26
  resolveDefaultModel,
27
+ modelMatches,
28
+ mergeModels,
29
+ applyModelOverrides,
30
+ normalizeDisabledModels,
24
31
  prettifyModelId,
25
32
  createAliasedProviderClass,
26
33
  getTierForModel
@@ -69,12 +76,19 @@ module.exports = {
69
76
 
70
77
  // Utilities
71
78
  testProviderAvailability,
79
+ resolveAvailabilityTimeoutMs,
80
+ secondsToTimeoutMs,
81
+ DEFAULT_AVAILABILITY_TIMEOUT_MS,
72
82
 
73
83
  // Config override support
74
84
  applyConfigOverrides,
75
85
  getProviderConfigOverrides,
76
86
  inferModelDefaults,
77
87
  resolveDefaultModel,
88
+ modelMatches,
89
+ mergeModels,
90
+ applyModelOverrides,
91
+ normalizeDisabledModels,
78
92
  prettifyModelId,
79
93
  getTierForModel,
80
94
 
@@ -587,9 +587,11 @@ class OpenCodeProvider extends AIProvider {
587
587
  /**
588
588
  * Test if OpenCode CLI is available
589
589
  * Uses the command configured in the instance (respects ENV > config > default precedence)
590
+ * @param {number} [timeoutMs=10000] - Timeout in milliseconds for the probe.
591
+ * Production passes the per-provider resolved value; the default is only hit by tests.
590
592
  * @returns {Promise<boolean>}
591
593
  */
592
- async testAvailability() {
594
+ async testAvailability(timeoutMs = 10000) {
593
595
  return new Promise((resolve) => {
594
596
  // For availability test, we just need to check --version
595
597
  // Use the already-resolved command from the constructor (this.opencodeCmd)
@@ -613,6 +615,16 @@ class OpenCodeProvider extends AIProvider {
613
615
  let stdout = '';
614
616
  let settled = false;
615
617
 
618
+ // Timeout guard: if the CLI hangs, kill it and resolve false so the probe
619
+ // does not leak a child process.
620
+ const availabilityTimeout = setTimeout(() => {
621
+ if (settled) return;
622
+ settled = true;
623
+ logger.warn(`OpenCode CLI availability check timed out after ${Math.round(timeoutMs / 1000)}s`);
624
+ try { opencode.kill(); } catch { /* ignore */ }
625
+ resolve(false);
626
+ }, timeoutMs);
627
+
616
628
  opencode.stdout.on('data', (data) => {
617
629
  stdout += data.toString();
618
630
  });
@@ -620,6 +632,7 @@ class OpenCodeProvider extends AIProvider {
620
632
  opencode.on('close', (code) => {
621
633
  if (settled) return;
622
634
  settled = true;
635
+ clearTimeout(availabilityTimeout);
623
636
  if (code === 0) {
624
637
  logger.info(`OpenCode CLI available: ${stdout.trim()}`);
625
638
  resolve(true);
@@ -632,6 +645,7 @@ class OpenCodeProvider extends AIProvider {
632
645
  opencode.on('error', (error) => {
633
646
  if (settled) return;
634
647
  settled = true;
648
+ clearTimeout(availabilityTimeout);
635
649
  logger.warn(`OpenCode CLI not available: ${error.message}`);
636
650
  resolve(false);
637
651
  });
@@ -1103,9 +1103,10 @@ class PiProvider extends AIProvider {
1103
1103
  /**
1104
1104
  * Test if Pi CLI is available
1105
1105
  * Uses the command configured in the instance (respects ENV > config > default precedence)
1106
+ * @param {number} [timeoutMs=10000] - Timeout in milliseconds for the probe
1106
1107
  * @returns {Promise<boolean>}
1107
1108
  */
1108
- async testAvailability() {
1109
+ async testAvailability(timeoutMs = 10000) {
1109
1110
  return new Promise((resolve) => {
1110
1111
  // For availability test, we just need to check --version
1111
1112
  // Use the already-resolved command from the constructor (this.piCmd)
@@ -1135,10 +1136,10 @@ class PiProvider extends AIProvider {
1135
1136
  const availabilityTimeout = setTimeout(() => {
1136
1137
  if (settled) return;
1137
1138
  settled = true;
1138
- logger.warn(`${name} CLI availability check timed out after 10s`);
1139
+ logger.warn(`${name} CLI availability check timed out after ${Math.round(timeoutMs / 1000)}s`);
1139
1140
  try { pi.kill(); } catch { /* ignore */ }
1140
1141
  resolve(false);
1141
- }, 10000);
1142
+ }, timeoutMs);
1142
1143
 
1143
1144
  pi.stdout.on('data', (data) => {
1144
1145
  stdout += data.toString();