rlm-cli 0.2.5 → 0.2.6

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 +72 -31
  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";
@@ -708,34 +732,48 @@ async function interactive() {
708
732
  if (!hasAnyApiKey()) {
709
733
  printBanner();
710
734
  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();
716
735
  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);
736
+ let setupDone = false;
737
+ while (!setupDone) {
738
+ console.log(` ${c.bold}Select your provider:${c.reset}\n`);
739
+ for (let i = 0; i < SETUP_PROVIDERS.length; i++) {
740
+ console.log(` ${c.dim}${i + 1}${c.reset} ${SETUP_PROVIDERS[i].name} ${c.dim}(${SETUP_PROVIDERS[i].label})${c.reset}`);
741
+ }
742
+ console.log();
743
+ const choice = await questionWithEsc(setupRl, ` ${c.cyan}Provider [1-${SETUP_PROVIDERS.length}]:${c.reset} `);
744
+ if (choice === null) {
745
+ // ESC at provider selection → exit
746
+ console.log(`\n ${c.dim}Exiting.${c.reset}\n`);
747
+ setupRl.close();
748
+ process.exit(0);
749
+ }
750
+ const idx = parseInt(choice, 10) - 1;
751
+ if (isNaN(idx) || idx < 0 || idx >= SETUP_PROVIDERS.length) {
752
+ console.log(`\n ${c.dim}Invalid choice.${c.reset}\n`);
753
+ continue;
754
+ }
755
+ const provider = SETUP_PROVIDERS[idx];
756
+ const gotKey = await promptForProviderKey(setupRl, provider);
757
+ if (gotKey === null) {
758
+ // ESC at key entry → back to provider selection
759
+ console.log();
760
+ continue;
761
+ }
762
+ if (!gotKey) {
763
+ console.log(`\n ${c.dim}No key provided. Exiting.${c.reset}\n`);
764
+ setupRl.close();
765
+ process.exit(0);
766
+ }
767
+ // Auto-select default model for chosen provider
768
+ const defaultModel = getDefaultModelForProvider(provider.piProvider);
769
+ if (defaultModel) {
770
+ currentModelId = defaultModel;
771
+ console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset}`);
772
+ }
773
+ console.log();
774
+ setupDone = true;
724
775
  }
725
- const provider = SETUP_PROVIDERS[idx];
726
- const gotKey = await promptForProviderKey(setupRl, provider);
727
776
  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
777
  }
740
778
  // Resolve model
741
779
  currentModel = resolveModel(currentModelId);
@@ -901,14 +939,17 @@ async function interactive() {
901
939
  const p = SETUP_PROVIDERS[i];
902
940
  const isCurrent = p.piProvider === curProvider;
903
941
  const dot = isCurrent ? `${c.green}●${c.reset}` : ` `;
904
- const hasKey = process.env[p.env] ? `${c.green}✓${c.reset}` : ` `;
942
+ // Only show for non-current providers that have a key
943
+ const hasKey = !isCurrent && process.env[p.env] ? `${c.green}✓${c.reset}` : ` `;
905
944
  const label = isCurrent
906
945
  ? `${c.cyan}${p.name}${c.reset} ${c.dim}(${p.label})${c.reset}`
907
946
  : `${p.name} ${c.dim}(${p.label})${c.reset}`;
908
947
  console.log(` ${c.dim}${i + 1}${c.reset} ${dot}${hasKey} ${label}`);
909
948
  }
910
949
  console.log();
911
- const provChoice = await new Promise((resolve) => rl.question(` ${c.cyan}Provider [1-${SETUP_PROVIDERS.length}]:${c.reset} `, (a) => resolve(a.trim())));
950
+ const provChoice = await questionWithEsc(rl, ` ${c.cyan}Provider [1-${SETUP_PROVIDERS.length}]:${c.reset} ${c.dim}(ESC to cancel)${c.reset} `);
951
+ if (provChoice === null)
952
+ break; // ESC
912
953
  const idx = parseInt(provChoice, 10) - 1;
913
954
  if (isNaN(idx) || idx < 0 || idx >= SETUP_PROVIDERS.length) {
914
955
  console.log(` ${c.dim}Cancelled.${c.reset}`);
@@ -917,7 +958,7 @@ async function interactive() {
917
958
  const chosen = SETUP_PROVIDERS[idx];
918
959
  const gotKey = await promptForProviderKey(rl, chosen);
919
960
  if (!gotKey) {
920
- console.log(` ${c.dim}Cancelled.${c.reset}`);
961
+ // null (ESC) or false (empty) → cancel
921
962
  break;
922
963
  }
923
964
  // 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.6",
4
4
  "description": "Standalone CLI for Recursive Language Models (RLMs) — implements Algorithm 1 from arXiv:2512.24601",
5
5
  "type": "module",
6
6
  "bin": {