rlm-cli 0.2.5 → 0.2.7

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.
Files changed (2) hide show
  1. package/dist/interactive.js +112 -34
  2. package/package.json +1 -1
@@ -157,13 +157,37 @@ function getDefaultModelForProvider(provider) {
157
157
  const models = getModels(provider);
158
158
  return models.length > 0 ? models[0].id : undefined;
159
159
  }
160
- /** Prompt user for a provider's API key if not already set. Returns true if key is available. */
160
+ /** Wrap rl.question with ESC-to-cancel. Returns user input or null on ESC/empty. */
161
+ function questionWithEsc(rlInstance, promptText) {
162
+ return new Promise((resolve) => {
163
+ let escaped = false;
164
+ const onKeypress = (_str, key) => {
165
+ if (key?.name === "escape" && !escaped) {
166
+ escaped = true;
167
+ stdin.removeListener("keypress", onKeypress);
168
+ // Clear the prompt line visually
169
+ process.stdout.write("\r\x1b[2K");
170
+ // Programmatically submit empty to close the pending question
171
+ rlInstance.write("\n");
172
+ }
173
+ };
174
+ stdin.on("keypress", onKeypress);
175
+ rlInstance.question(promptText, (answer) => {
176
+ stdin.removeListener("keypress", onKeypress);
177
+ resolve(escaped ? null : answer.trim() || null);
178
+ });
179
+ });
180
+ }
181
+ /** Prompt user for a provider's API key if not already set.
182
+ * Returns true (got key), false (empty input), or null (ESC pressed). */
161
183
  async function promptForProviderKey(rlInstance, providerInfo) {
162
184
  if (process.env[providerInfo.env])
163
185
  return true;
164
- const key = await new Promise((resolve) => rlInstance.question(` ${c.cyan}${providerInfo.env}:${c.reset} `, (a) => resolve(a.trim())));
186
+ const key = await questionWithEsc(rlInstance, ` ${c.cyan}${providerInfo.env}:${c.reset} `);
187
+ if (key === null)
188
+ return null; // ESC
165
189
  if (!key)
166
- return false;
190
+ return false; // empty
167
191
  process.env[providerInfo.env] = key;
168
192
  // Save to shell profile
169
193
  const shellRc = process.env.SHELL?.includes("zsh") ? "~/.zshrc" : "~/.bashrc";
@@ -440,6 +464,41 @@ function displaySubQueryResult(info) {
440
464
  console.log(` ${c.magenta}└─${c.reset} ${c.dim}${elapsed}s · ${formatSize(info.resultLength)} received${c.reset}`);
441
465
  }
442
466
  // ── Available models list ────────────────────────────────────────────────────
467
+ /** Filter out deprecated, legacy, preview-only, and non-chat models. */
468
+ const EXCLUDED_MODEL_PATTERNS = [
469
+ // Anthropic legacy
470
+ /^claude-3-haiku/,
471
+ /^claude-3-sonnet/,
472
+ /^claude-3-opus/,
473
+ /^claude-3-5-sonnet-20240620/, // superseded by v2
474
+ // OpenAI legacy
475
+ /^gpt-4$/, // base gpt-4
476
+ /^gpt-4-turbo/, // gpt-4-turbo
477
+ /^gpt-4o-2024-/, // old dated snapshots
478
+ // Non-chat / specialized
479
+ /^codex-/,
480
+ /^gpt-5\.\d+-codex/,
481
+ /^gpt-5-codex/,
482
+ /^gpt-5\.3-codex/,
483
+ /deep-research$/,
484
+ /^gemini-live-/,
485
+ /^grok-beta$/,
486
+ /^grok-vision-beta$/,
487
+ /^grok-2-vision/,
488
+ // Old/niche Mistral
489
+ /^open-mistral-7b$/,
490
+ /^open-mixtral-/,
491
+ /^mistral-nemo$/,
492
+ // Dated previews (keep only -latest and stable)
493
+ /preview-\d{2}-\d{2}$/, // e.g. preview-04-17, preview-05-20
494
+ /preview-\d{2}-\d{4}$/, // e.g. preview-09-2025
495
+ /^labs-/,
496
+ // Specialized/internal variants
497
+ /-customtools$/,
498
+ ];
499
+ function isModelExcluded(modelId) {
500
+ return EXCLUDED_MODEL_PATTERNS.some((p) => p.test(modelId));
501
+ }
443
502
  /** Collect models from providers that have an API key set. */
444
503
  function getAvailableModels() {
445
504
  const items = [];
@@ -448,7 +507,8 @@ function getAvailableModels() {
448
507
  if (!process.env[key] && provider !== detectProvider())
449
508
  continue;
450
509
  for (const m of getModels(provider)) {
451
- items.push({ id: m.id, provider });
510
+ if (!isModelExcluded(m.id))
511
+ items.push({ id: m.id, provider });
452
512
  }
453
513
  }
454
514
  return items;
@@ -460,7 +520,8 @@ function getModelsForProvider(providerName) {
460
520
  if (provider !== providerName)
461
521
  continue;
462
522
  for (const m of getModels(provider)) {
463
- items.push({ id: m.id, provider });
523
+ if (!isModelExcluded(m.id))
524
+ items.push({ id: m.id, provider });
464
525
  }
465
526
  }
466
527
  return items;
@@ -707,35 +768,49 @@ async function interactive() {
707
768
  // Validate env
708
769
  if (!hasAnyApiKey()) {
709
770
  printBanner();
710
- console.log(` ${c.red}No API key found.${c.reset}\n`);
711
- console.log(` ${c.bold}Select your provider:${c.reset}\n`);
712
- for (let i = 0; i < SETUP_PROVIDERS.length; i++) {
713
- console.log(` ${c.dim}${i + 1}${c.reset} ${SETUP_PROVIDERS[i].name} ${c.dim}(${SETUP_PROVIDERS[i].label})${c.reset}`);
714
- }
715
- console.log();
771
+ console.log(` ${c.bold}Welcome! Let's get you set up.${c.reset}\n`);
716
772
  const setupRl = readline.createInterface({ input: stdin, output: stdout, terminal: true });
717
- const ask = (prompt) => new Promise((resolve) => setupRl.question(prompt, (a) => resolve(a.trim())));
718
- const choice = await ask(` ${c.cyan}Provider [1-${SETUP_PROVIDERS.length}]:${c.reset} `);
719
- const idx = parseInt(choice, 10) - 1;
720
- if (isNaN(idx) || idx < 0 || idx >= SETUP_PROVIDERS.length) {
721
- console.log(`\n ${c.dim}Invalid choice. Exiting.${c.reset}\n`);
722
- setupRl.close();
723
- process.exit(0);
773
+ let setupDone = false;
774
+ while (!setupDone) {
775
+ console.log(` ${c.bold}Select your provider:${c.reset}\n`);
776
+ for (let i = 0; i < SETUP_PROVIDERS.length; i++) {
777
+ console.log(` ${c.dim}${i + 1}${c.reset} ${SETUP_PROVIDERS[i].name} ${c.dim}(${SETUP_PROVIDERS[i].label})${c.reset}`);
778
+ }
779
+ console.log();
780
+ const choice = await questionWithEsc(setupRl, ` ${c.cyan}Provider [1-${SETUP_PROVIDERS.length}]:${c.reset} `);
781
+ if (choice === null) {
782
+ // ESC at provider selection → exit
783
+ console.log(`\n ${c.dim}Exiting.${c.reset}\n`);
784
+ setupRl.close();
785
+ process.exit(0);
786
+ }
787
+ const idx = parseInt(choice, 10) - 1;
788
+ if (isNaN(idx) || idx < 0 || idx >= SETUP_PROVIDERS.length) {
789
+ console.log(`\n ${c.dim}Invalid choice.${c.reset}\n`);
790
+ continue;
791
+ }
792
+ const provider = SETUP_PROVIDERS[idx];
793
+ const gotKey = await promptForProviderKey(setupRl, provider);
794
+ if (gotKey === null) {
795
+ // ESC at key entry → back to provider selection
796
+ console.log();
797
+ continue;
798
+ }
799
+ if (!gotKey) {
800
+ console.log(`\n ${c.dim}No key provided. Exiting.${c.reset}\n`);
801
+ setupRl.close();
802
+ process.exit(0);
803
+ }
804
+ // Auto-select default model for chosen provider
805
+ const defaultModel = getDefaultModelForProvider(provider.piProvider);
806
+ if (defaultModel) {
807
+ currentModelId = defaultModel;
808
+ console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset}`);
809
+ }
810
+ console.log();
811
+ setupDone = true;
724
812
  }
725
- const provider = SETUP_PROVIDERS[idx];
726
- const gotKey = await promptForProviderKey(setupRl, provider);
727
813
  setupRl.close();
728
- if (!gotKey) {
729
- console.log(`\n ${c.dim}No key provided. Exiting.${c.reset}\n`);
730
- process.exit(0);
731
- }
732
- // Auto-select default model for chosen provider
733
- const defaultModel = getDefaultModelForProvider(provider.piProvider);
734
- if (defaultModel) {
735
- currentModelId = defaultModel;
736
- console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset}`);
737
- }
738
- console.log();
739
814
  }
740
815
  // Resolve model
741
816
  currentModel = resolveModel(currentModelId);
@@ -901,14 +976,17 @@ async function interactive() {
901
976
  const p = SETUP_PROVIDERS[i];
902
977
  const isCurrent = p.piProvider === curProvider;
903
978
  const dot = isCurrent ? `${c.green}●${c.reset}` : ` `;
904
- const hasKey = process.env[p.env] ? `${c.green}✓${c.reset}` : ` `;
979
+ // Only show for non-current providers that have a key
980
+ const hasKey = !isCurrent && process.env[p.env] ? `${c.green}✓${c.reset}` : ` `;
905
981
  const label = isCurrent
906
982
  ? `${c.cyan}${p.name}${c.reset} ${c.dim}(${p.label})${c.reset}`
907
983
  : `${p.name} ${c.dim}(${p.label})${c.reset}`;
908
984
  console.log(` ${c.dim}${i + 1}${c.reset} ${dot}${hasKey} ${label}`);
909
985
  }
910
986
  console.log();
911
- const provChoice = await new Promise((resolve) => rl.question(` ${c.cyan}Provider [1-${SETUP_PROVIDERS.length}]:${c.reset} `, (a) => resolve(a.trim())));
987
+ const provChoice = await questionWithEsc(rl, ` ${c.cyan}Provider [1-${SETUP_PROVIDERS.length}]:${c.reset} ${c.dim}(ESC to cancel)${c.reset} `);
988
+ if (provChoice === null)
989
+ break; // ESC
912
990
  const idx = parseInt(provChoice, 10) - 1;
913
991
  if (isNaN(idx) || idx < 0 || idx >= SETUP_PROVIDERS.length) {
914
992
  console.log(` ${c.dim}Cancelled.${c.reset}`);
@@ -917,7 +995,7 @@ async function interactive() {
917
995
  const chosen = SETUP_PROVIDERS[idx];
918
996
  const gotKey = await promptForProviderKey(rl, chosen);
919
997
  if (!gotKey) {
920
- console.log(` ${c.dim}Cancelled.${c.reset}`);
998
+ // null (ESC) or false (empty) → cancel
921
999
  break;
922
1000
  }
923
1001
  // Auto-select first model from new provider
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rlm-cli",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Standalone CLI for Recursive Language Models (RLMs) — implements Algorithm 1 from arXiv:2512.24601",
5
5
  "type": "module",
6
6
  "bin": {