rlm-cli 0.2.9 → 0.2.11
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/dist/env.d.ts +4 -4
- package/dist/env.js +16 -11
- package/dist/interactive.js +288 -245
- package/package.json +1 -1
package/dist/env.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Load
|
|
2
|
+
* Load env vars into process.env.
|
|
3
3
|
* Must be imported BEFORE any module that reads env vars (e.g. pi-ai).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
7
|
-
*
|
|
5
|
+
* Load order (later wins):
|
|
6
|
+
* 1. ~/.rlm/credentials — persistent keys saved by first-run setup
|
|
7
|
+
* 2. .env in package root — local overrides
|
|
8
8
|
*/
|
|
9
9
|
export {};
|
package/dist/env.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Load
|
|
2
|
+
* Load env vars into process.env.
|
|
3
3
|
* Must be imported BEFORE any module that reads env vars (e.g. pi-ai).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
7
|
-
*
|
|
5
|
+
* Load order (later wins):
|
|
6
|
+
* 1. ~/.rlm/credentials — persistent keys saved by first-run setup
|
|
7
|
+
* 2. .env in package root — local overrides
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import * as path from "node:path";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const content = fs.readFileSync(
|
|
11
|
+
import * as os from "node:os";
|
|
12
|
+
function loadEnvFile(filePath) {
|
|
13
|
+
if (!fs.existsSync(filePath))
|
|
14
|
+
return;
|
|
15
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
16
16
|
for (const line of content.split("\n")) {
|
|
17
17
|
const trimmed = line.trim();
|
|
18
18
|
if (!trimmed || trimmed.startsWith("#"))
|
|
@@ -22,13 +22,18 @@ if (fs.existsSync(envPath)) {
|
|
|
22
22
|
continue;
|
|
23
23
|
const key = trimmed.slice(0, eqIndex).trim();
|
|
24
24
|
const value = trimmed.slice(eqIndex + 1).trim();
|
|
25
|
-
if (key) {
|
|
25
|
+
if (key && !process.env[key]) {
|
|
26
26
|
process.env[key] = value;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
+
// 1. Load persistent credentials (~/.rlm/credentials)
|
|
31
|
+
loadEnvFile(path.join(os.homedir(), ".rlm", "credentials"));
|
|
32
|
+
// 2. Load .env from package root (local overrides)
|
|
33
|
+
const __dir = path.dirname(new URL(import.meta.url).pathname);
|
|
34
|
+
loadEnvFile(path.resolve(__dir, "..", ".env"));
|
|
30
35
|
// Default model
|
|
31
36
|
if (!process.env.RLM_MODEL) {
|
|
32
|
-
process.env.RLM_MODEL = "claude-sonnet-4-
|
|
37
|
+
process.env.RLM_MODEL = "claude-sonnet-4-6";
|
|
33
38
|
}
|
|
34
39
|
//# sourceMappingURL=env.js.map
|
package/dist/interactive.js
CHANGED
|
@@ -10,8 +10,18 @@
|
|
|
10
10
|
import "./env.js";
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import * as os from "node:os";
|
|
13
14
|
import * as readline from "node:readline";
|
|
14
15
|
import { stdin, stdout } from "node:process";
|
|
16
|
+
// Global error handlers — prevent raw stack traces from leaking to terminal
|
|
17
|
+
process.on("uncaughtException", (err) => {
|
|
18
|
+
console.error(`\n \x1b[31mUnexpected error: ${err.message}\x1b[0m\n`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
|
21
|
+
process.on("unhandledRejection", (err) => {
|
|
22
|
+
console.error(`\n \x1b[31mUnexpected error: ${err?.message || err}\x1b[0m\n`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
});
|
|
15
25
|
const { getModels, getProviders } = await import("@mariozechner/pi-ai");
|
|
16
26
|
const { PythonRepl } = await import("./repl.js");
|
|
17
27
|
const { runRlmLoop } = await import("./rlm.js");
|
|
@@ -68,7 +78,8 @@ class Spinner {
|
|
|
68
78
|
}
|
|
69
79
|
// ── Constants ───────────────────────────────────────────────────────────────
|
|
70
80
|
const DEFAULT_MODEL = process.env.RLM_MODEL || "claude-sonnet-4-6";
|
|
71
|
-
const
|
|
81
|
+
const RLM_HOME = path.join(os.homedir(), ".rlm");
|
|
82
|
+
const TRAJ_DIR = path.join(RLM_HOME, "trajectories");
|
|
72
83
|
const W = Math.min(process.stdout.columns || 80, 100);
|
|
73
84
|
// ── Session state ───────────────────────────────────────────────────────────
|
|
74
85
|
let currentModelId = DEFAULT_MODEL;
|
|
@@ -200,21 +211,31 @@ function questionWithEsc(rlInstance, promptText) {
|
|
|
200
211
|
async function promptForProviderKey(rlInstance, providerInfo) {
|
|
201
212
|
if (process.env[providerInfo.env])
|
|
202
213
|
return true;
|
|
203
|
-
const
|
|
204
|
-
if (
|
|
214
|
+
const rawKey = await questionWithEsc(rlInstance, ` ${c.cyan}${providerInfo.env}:${c.reset} `);
|
|
215
|
+
if (rawKey === null)
|
|
205
216
|
return null; // ESC
|
|
206
|
-
if (!
|
|
217
|
+
if (!rawKey)
|
|
207
218
|
return false; // empty
|
|
219
|
+
// Sanitize: strip newlines, control chars, whitespace
|
|
220
|
+
const key = rawKey.replace(/[\r\n\x00-\x1f]/g, "").trim();
|
|
221
|
+
if (!key)
|
|
222
|
+
return false;
|
|
208
223
|
process.env[providerInfo.env] = key;
|
|
209
|
-
// Save to
|
|
210
|
-
const
|
|
211
|
-
const rcPath = shellRc.replace("~", process.env.HOME || "~");
|
|
224
|
+
// Save to ~/.rlm/credentials (persistent across sessions)
|
|
225
|
+
const credPath = path.join(RLM_HOME, "credentials");
|
|
212
226
|
try {
|
|
213
|
-
fs.
|
|
214
|
-
|
|
227
|
+
if (!fs.existsSync(RLM_HOME))
|
|
228
|
+
fs.mkdirSync(RLM_HOME, { recursive: true });
|
|
229
|
+
fs.appendFileSync(credPath, `${providerInfo.env}=${key}\n`);
|
|
230
|
+
// Restrict permissions (owner-only read/write)
|
|
231
|
+
try {
|
|
232
|
+
fs.chmodSync(credPath, 0o600);
|
|
233
|
+
}
|
|
234
|
+
catch { /* Windows etc. */ }
|
|
235
|
+
console.log(`\n ${c.green}✓${c.reset} ${providerInfo.name} key saved to ${c.dim}~/.rlm/credentials${c.reset}`);
|
|
215
236
|
}
|
|
216
237
|
catch {
|
|
217
|
-
console.log(`\n ${c.yellow}!${c.reset} Could not
|
|
238
|
+
console.log(`\n ${c.yellow}!${c.reset} Could not save key. Add manually:`);
|
|
218
239
|
console.log(` ${c.yellow}export ${providerInfo.env}=${key}${c.reset}`);
|
|
219
240
|
}
|
|
220
241
|
return true;
|
|
@@ -308,10 +329,15 @@ async function handleFile(arg) {
|
|
|
308
329
|
console.log(` ${c.red}File not found: ${filePath}${c.reset}`);
|
|
309
330
|
return;
|
|
310
331
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
332
|
+
try {
|
|
333
|
+
contextText = fs.readFileSync(filePath, "utf-8");
|
|
334
|
+
contextSource = arg;
|
|
335
|
+
const lines = contextText.split("\n").length;
|
|
336
|
+
console.log(` ${c.green}✓${c.reset} Loaded ${c.bold}${contextText.length.toLocaleString()}${c.reset} chars (${lines.toLocaleString()} lines) from ${c.underline}${arg}${c.reset}`);
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
console.log(` ${c.red}Could not read file: ${err.message}${c.reset}`);
|
|
340
|
+
}
|
|
315
341
|
}
|
|
316
342
|
async function handleUrl(arg) {
|
|
317
343
|
if (!arg) {
|
|
@@ -687,12 +713,17 @@ async function runQuery(query) {
|
|
|
687
713
|
console.log(boxBottom(c.green));
|
|
688
714
|
console.log();
|
|
689
715
|
// Save trajectory
|
|
690
|
-
|
|
691
|
-
fs.
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
716
|
+
try {
|
|
717
|
+
if (!fs.existsSync(TRAJ_DIR))
|
|
718
|
+
fs.mkdirSync(TRAJ_DIR, { recursive: true });
|
|
719
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
720
|
+
const trajFile = `trajectory-${ts}.json`;
|
|
721
|
+
fs.writeFileSync(path.join(TRAJ_DIR, trajFile), JSON.stringify(trajectory, null, 2), "utf-8");
|
|
722
|
+
console.log(` ${c.dim}Saved: ~/.rlm/trajectories/${trajFile}${c.reset}\n`);
|
|
723
|
+
}
|
|
724
|
+
catch {
|
|
725
|
+
console.log(` ${c.yellow}Could not save trajectory.${c.reset}\n`);
|
|
726
|
+
}
|
|
696
727
|
}
|
|
697
728
|
catch (err) {
|
|
698
729
|
spinner.stop();
|
|
@@ -751,11 +782,17 @@ function expandAtFiles(input) {
|
|
|
751
782
|
if (atMatch) {
|
|
752
783
|
const filePath = path.resolve(atMatch[1]);
|
|
753
784
|
if (fs.existsSync(filePath)) {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
785
|
+
try {
|
|
786
|
+
contextText = fs.readFileSync(filePath, "utf-8");
|
|
787
|
+
contextSource = atMatch[1];
|
|
788
|
+
const lines = contextText.split("\n").length;
|
|
789
|
+
console.log(` ${c.green}✓${c.reset} Loaded ${c.bold}${contextText.length.toLocaleString()}${c.reset} chars (${lines} lines) from ${c.underline}${atMatch[1]}${c.reset}`);
|
|
790
|
+
return atMatch[2] || "";
|
|
791
|
+
}
|
|
792
|
+
catch (err) {
|
|
793
|
+
console.log(` ${c.red}Could not read file: ${err.message}${c.reset}`);
|
|
794
|
+
return "";
|
|
795
|
+
}
|
|
759
796
|
}
|
|
760
797
|
else {
|
|
761
798
|
console.log(` ${c.red}File not found: ${atMatch[1]}${c.reset}`);
|
|
@@ -867,251 +904,257 @@ async function interactive() {
|
|
|
867
904
|
};
|
|
868
905
|
rl.prompt();
|
|
869
906
|
rl.on("line", async (rawLine) => {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
907
|
+
try {
|
|
908
|
+
if (isRunning)
|
|
909
|
+
return; // ignore input while a query is active
|
|
910
|
+
const line = rawLine.trim();
|
|
911
|
+
// URL auto-detect
|
|
912
|
+
if (line.startsWith("http://") || line.startsWith("https://")) {
|
|
913
|
+
const loaded = await detectAndLoadUrl(line);
|
|
914
|
+
if (loaded) {
|
|
915
|
+
printStatusLine();
|
|
916
|
+
console.log(`\n ${c.dim}Now type your query...${c.reset}\n`);
|
|
917
|
+
rl.prompt();
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
881
920
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
921
|
+
// Multi-line paste detect
|
|
922
|
+
if (isMultiLineInput(rawLine)) {
|
|
923
|
+
const result = handleMultiLineAsContext(rawLine);
|
|
924
|
+
if (result) {
|
|
925
|
+
contextText = result.context;
|
|
926
|
+
contextSource = "(pasted)";
|
|
927
|
+
printStatusLine();
|
|
928
|
+
console.log(`\n ${c.dim}Now type your query...${c.reset}\n`);
|
|
929
|
+
rl.prompt();
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (!line) {
|
|
891
934
|
rl.prompt();
|
|
892
935
|
return;
|
|
893
936
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
937
|
+
// Slash commands
|
|
938
|
+
if (line.startsWith("/")) {
|
|
939
|
+
const [cmd, ...rest] = line.slice(1).split(/\s+/);
|
|
940
|
+
const arg = rest.join(" ");
|
|
941
|
+
switch (cmd) {
|
|
942
|
+
case "help":
|
|
943
|
+
case "h":
|
|
944
|
+
printCommandHelp();
|
|
945
|
+
break;
|
|
946
|
+
case "file":
|
|
947
|
+
case "f":
|
|
948
|
+
await handleFile(arg);
|
|
949
|
+
break;
|
|
950
|
+
case "url":
|
|
951
|
+
case "u":
|
|
952
|
+
await handleUrl(arg);
|
|
953
|
+
break;
|
|
954
|
+
case "paste":
|
|
955
|
+
case "p":
|
|
956
|
+
await handlePaste(rl);
|
|
957
|
+
break;
|
|
958
|
+
case "context":
|
|
959
|
+
case "ctx":
|
|
960
|
+
handleContext();
|
|
961
|
+
break;
|
|
962
|
+
case "clear-context":
|
|
963
|
+
case "cc":
|
|
964
|
+
contextText = "";
|
|
965
|
+
contextSource = "";
|
|
966
|
+
console.log(` ${c.green}✓${c.reset} Context cleared.`);
|
|
967
|
+
break;
|
|
968
|
+
case "model":
|
|
969
|
+
case "m": {
|
|
970
|
+
const curProvider = detectProvider();
|
|
971
|
+
if (arg) {
|
|
972
|
+
// Accept a number (from current provider list) or a model ID
|
|
973
|
+
const curModels = getModelsForProvider(curProvider);
|
|
974
|
+
let pick;
|
|
975
|
+
if (/^\d+$/.test(arg)) {
|
|
976
|
+
pick = curModels[parseInt(arg, 10) - 1]?.id;
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
pick = arg;
|
|
980
|
+
}
|
|
981
|
+
if (!pick) {
|
|
982
|
+
console.log(` ${c.red}Invalid selection.${c.reset} Use ${c.cyan}/model${c.reset} to list available models.`);
|
|
983
|
+
break;
|
|
984
|
+
}
|
|
985
|
+
// Check if this model belongs to a different provider
|
|
986
|
+
const resolved = resolveModelWithProvider(pick);
|
|
987
|
+
if (!resolved) {
|
|
988
|
+
console.log(` ${c.red}Model "${arg}" not found.${c.reset} Use ${c.cyan}/model${c.reset} to list available models.`);
|
|
989
|
+
break;
|
|
990
|
+
}
|
|
991
|
+
if (resolved.provider !== curProvider) {
|
|
992
|
+
// Cross-provider switch
|
|
993
|
+
const setupInfo = findSetupProvider(resolved.provider);
|
|
994
|
+
const envVar = setupInfo?.env || providerEnvKey(resolved.provider);
|
|
995
|
+
const provName = setupInfo?.name || resolved.provider;
|
|
996
|
+
if (!process.env[envVar]) {
|
|
997
|
+
console.log(` ${c.yellow}That model requires ${provName}.${c.reset}`);
|
|
998
|
+
const gotKey = await promptForProviderKey(rl, { name: provName, env: envVar });
|
|
999
|
+
if (!gotKey) {
|
|
1000
|
+
console.log(` ${c.dim}Cancelled.${c.reset}`);
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
currentModelId = pick;
|
|
1006
|
+
currentModel = resolved.model;
|
|
1007
|
+
console.log(` ${c.green}✓${c.reset} Switched to ${c.bold}${currentModelId}${c.reset}`);
|
|
1008
|
+
console.log();
|
|
1009
|
+
printStatusLine();
|
|
939
1010
|
}
|
|
940
1011
|
else {
|
|
941
|
-
|
|
1012
|
+
// List models for current provider
|
|
1013
|
+
const models = getModelsForProvider(curProvider);
|
|
1014
|
+
const provLabel = findSetupProvider(curProvider)?.name || curProvider;
|
|
1015
|
+
console.log(`\n ${c.bold}Current model:${c.reset} ${c.cyan}${currentModelId}${c.reset} ${c.dim}(${provLabel})${c.reset}\n`);
|
|
1016
|
+
const pad = String(models.length).length;
|
|
1017
|
+
for (let i = 0; i < models.length; i++) {
|
|
1018
|
+
const m = models[i];
|
|
1019
|
+
const num = String(i + 1).padStart(pad);
|
|
1020
|
+
const dot = m.id === currentModelId ? `${c.green}●${c.reset}` : ` `;
|
|
1021
|
+
const label = m.id === currentModelId
|
|
1022
|
+
? `${c.cyan}${m.id}${c.reset}`
|
|
1023
|
+
: `${c.dim}${m.id}${c.reset}`;
|
|
1024
|
+
console.log(` ${c.dim}${num}${c.reset} ${dot} ${label}`);
|
|
1025
|
+
}
|
|
1026
|
+
console.log(`\n ${c.dim}${models.length} models · scroll up to see full list.${c.reset}`);
|
|
1027
|
+
console.log(` ${c.dim}Type${c.reset} ${c.cyan}/model <number>${c.reset} ${c.dim}or${c.reset} ${c.cyan}/model <id>${c.reset} ${c.dim}to switch.${c.reset}`);
|
|
1028
|
+
console.log(` ${c.dim}Type${c.reset} ${c.cyan}/provider${c.reset} ${c.dim}to switch provider.${c.reset}`);
|
|
1029
|
+
}
|
|
1030
|
+
break;
|
|
1031
|
+
}
|
|
1032
|
+
case "provider":
|
|
1033
|
+
case "prov": {
|
|
1034
|
+
const curProvider = detectProvider();
|
|
1035
|
+
const curLabel = findSetupProvider(curProvider)?.name || curProvider;
|
|
1036
|
+
console.log(`\n ${c.bold}Current provider:${c.reset} ${c.cyan}${curLabel}${c.reset}\n`);
|
|
1037
|
+
for (let i = 0; i < SETUP_PROVIDERS.length; i++) {
|
|
1038
|
+
const p = SETUP_PROVIDERS[i];
|
|
1039
|
+
const isCurrent = p.piProvider === curProvider;
|
|
1040
|
+
const dot = isCurrent ? `${c.green}●${c.reset}` : ` `;
|
|
1041
|
+
// Only show ✓ for non-current providers that have a key
|
|
1042
|
+
const hasKey = !isCurrent && process.env[p.env] ? `${c.green}✓${c.reset}` : ` `;
|
|
1043
|
+
const label = isCurrent
|
|
1044
|
+
? `${c.cyan}${p.name}${c.reset} ${c.dim}(${p.label})${c.reset}`
|
|
1045
|
+
: `${p.name} ${c.dim}(${p.label})${c.reset}`;
|
|
1046
|
+
console.log(` ${c.dim}${i + 1}${c.reset} ${dot}${hasKey} ${label}`);
|
|
942
1047
|
}
|
|
943
|
-
|
|
944
|
-
|
|
1048
|
+
console.log();
|
|
1049
|
+
const provChoice = await questionWithEsc(rl, ` ${c.cyan}Provider [1-${SETUP_PROVIDERS.length}]:${c.reset} ${c.dim}(ESC to cancel)${c.reset} `);
|
|
1050
|
+
if (provChoice === null)
|
|
1051
|
+
break; // ESC
|
|
1052
|
+
const idx = parseInt(provChoice, 10) - 1;
|
|
1053
|
+
if (isNaN(idx) || idx < 0 || idx >= SETUP_PROVIDERS.length) {
|
|
1054
|
+
console.log(` ${c.dim}Cancelled.${c.reset}`);
|
|
945
1055
|
break;
|
|
946
1056
|
}
|
|
947
|
-
|
|
948
|
-
const
|
|
949
|
-
if (!
|
|
950
|
-
|
|
1057
|
+
const chosen = SETUP_PROVIDERS[idx];
|
|
1058
|
+
const gotKey = await promptForProviderKey(rl, chosen);
|
|
1059
|
+
if (!gotKey) {
|
|
1060
|
+
// null (ESC) or false (empty) → cancel
|
|
951
1061
|
break;
|
|
952
1062
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
console.log(` ${c.dim}Cancelled.${c.reset}`);
|
|
963
|
-
break;
|
|
964
|
-
}
|
|
965
|
-
}
|
|
1063
|
+
// Auto-select first model from new provider
|
|
1064
|
+
const defaultModel = getDefaultModelForProvider(chosen.piProvider);
|
|
1065
|
+
if (defaultModel) {
|
|
1066
|
+
currentModelId = defaultModel;
|
|
1067
|
+
currentModel = resolveModel(currentModelId);
|
|
1068
|
+
console.log(` ${c.green}✓${c.reset} Switched to ${c.bold}${chosen.name}${c.reset}`);
|
|
1069
|
+
console.log(` ${c.green}✓${c.reset} Default model: ${c.bold}${currentModelId}${c.reset}`);
|
|
1070
|
+
console.log();
|
|
1071
|
+
printStatusLine();
|
|
966
1072
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
console.log(` ${c.green}✓${c.reset} Switched to ${c.bold}${currentModelId}${c.reset}`);
|
|
970
|
-
console.log();
|
|
971
|
-
printStatusLine();
|
|
972
|
-
}
|
|
973
|
-
else {
|
|
974
|
-
// List models for current provider
|
|
975
|
-
const models = getModelsForProvider(curProvider);
|
|
976
|
-
const provLabel = findSetupProvider(curProvider)?.name || curProvider;
|
|
977
|
-
console.log(`\n ${c.bold}Current model:${c.reset} ${c.cyan}${currentModelId}${c.reset} ${c.dim}(${provLabel})${c.reset}\n`);
|
|
978
|
-
const pad = String(models.length).length;
|
|
979
|
-
for (let i = 0; i < models.length; i++) {
|
|
980
|
-
const m = models[i];
|
|
981
|
-
const num = String(i + 1).padStart(pad);
|
|
982
|
-
const dot = m.id === currentModelId ? `${c.green}●${c.reset}` : ` `;
|
|
983
|
-
const label = m.id === currentModelId
|
|
984
|
-
? `${c.cyan}${m.id}${c.reset}`
|
|
985
|
-
: `${c.dim}${m.id}${c.reset}`;
|
|
986
|
-
console.log(` ${c.dim}${num}${c.reset} ${dot} ${label}`);
|
|
1073
|
+
else {
|
|
1074
|
+
console.log(` ${c.red}No models available for ${chosen.name}.${c.reset}`);
|
|
987
1075
|
}
|
|
988
|
-
console.log(`\n ${c.dim}${models.length} models · scroll up to see full list.${c.reset}`);
|
|
989
|
-
console.log(` ${c.dim}Type${c.reset} ${c.cyan}/model <number>${c.reset} ${c.dim}or${c.reset} ${c.cyan}/model <id>${c.reset} ${c.dim}to switch.${c.reset}`);
|
|
990
|
-
console.log(` ${c.dim}Type${c.reset} ${c.cyan}/provider${c.reset} ${c.dim}to switch provider.${c.reset}`);
|
|
991
|
-
}
|
|
992
|
-
break;
|
|
993
|
-
}
|
|
994
|
-
case "provider":
|
|
995
|
-
case "prov": {
|
|
996
|
-
const curProvider = detectProvider();
|
|
997
|
-
const curLabel = findSetupProvider(curProvider)?.name || curProvider;
|
|
998
|
-
console.log(`\n ${c.bold}Current provider:${c.reset} ${c.cyan}${curLabel}${c.reset}\n`);
|
|
999
|
-
for (let i = 0; i < SETUP_PROVIDERS.length; i++) {
|
|
1000
|
-
const p = SETUP_PROVIDERS[i];
|
|
1001
|
-
const isCurrent = p.piProvider === curProvider;
|
|
1002
|
-
const dot = isCurrent ? `${c.green}●${c.reset}` : ` `;
|
|
1003
|
-
// Only show ✓ for non-current providers that have a key
|
|
1004
|
-
const hasKey = !isCurrent && process.env[p.env] ? `${c.green}✓${c.reset}` : ` `;
|
|
1005
|
-
const label = isCurrent
|
|
1006
|
-
? `${c.cyan}${p.name}${c.reset} ${c.dim}(${p.label})${c.reset}`
|
|
1007
|
-
: `${p.name} ${c.dim}(${p.label})${c.reset}`;
|
|
1008
|
-
console.log(` ${c.dim}${i + 1}${c.reset} ${dot}${hasKey} ${label}`);
|
|
1009
|
-
}
|
|
1010
|
-
console.log();
|
|
1011
|
-
const provChoice = await questionWithEsc(rl, ` ${c.cyan}Provider [1-${SETUP_PROVIDERS.length}]:${c.reset} ${c.dim}(ESC to cancel)${c.reset} `);
|
|
1012
|
-
if (provChoice === null)
|
|
1013
|
-
break; // ESC
|
|
1014
|
-
const idx = parseInt(provChoice, 10) - 1;
|
|
1015
|
-
if (isNaN(idx) || idx < 0 || idx >= SETUP_PROVIDERS.length) {
|
|
1016
|
-
console.log(` ${c.dim}Cancelled.${c.reset}`);
|
|
1017
1076
|
break;
|
|
1018
1077
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
// null (ESC) or false (empty) → cancel
|
|
1078
|
+
case "trajectories":
|
|
1079
|
+
case "traj":
|
|
1080
|
+
handleTrajectories();
|
|
1023
1081
|
break;
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
console.log(
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
else {
|
|
1036
|
-
console.log(` ${c.red}No models available for ${chosen.name}.${c.reset}`);
|
|
1037
|
-
}
|
|
1038
|
-
break;
|
|
1082
|
+
case "clear":
|
|
1083
|
+
printWelcome();
|
|
1084
|
+
break;
|
|
1085
|
+
case "quit":
|
|
1086
|
+
case "q":
|
|
1087
|
+
case "exit":
|
|
1088
|
+
console.log(`\n ${c.dim}Goodbye!${c.reset}\n`);
|
|
1089
|
+
process.exit(0);
|
|
1090
|
+
break;
|
|
1091
|
+
default:
|
|
1092
|
+
console.log(` ${c.red}Unknown command: /${cmd}${c.reset}. Type ${c.cyan}/help${c.reset} for commands.`);
|
|
1039
1093
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
handleTrajectories();
|
|
1043
|
-
break;
|
|
1044
|
-
case "clear":
|
|
1045
|
-
printWelcome();
|
|
1046
|
-
break;
|
|
1047
|
-
case "quit":
|
|
1048
|
-
case "q":
|
|
1049
|
-
case "exit":
|
|
1050
|
-
console.log(`\n ${c.dim}Goodbye!${c.reset}\n`);
|
|
1051
|
-
process.exit(0);
|
|
1052
|
-
break;
|
|
1053
|
-
default:
|
|
1054
|
-
console.log(` ${c.red}Unknown command: /${cmd}${c.reset}. Type ${c.cyan}/help${c.reset} for commands.`);
|
|
1094
|
+
rl.prompt();
|
|
1095
|
+
return;
|
|
1055
1096
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1097
|
+
// @file shorthand
|
|
1098
|
+
let query = expandAtFiles(line);
|
|
1099
|
+
if (!query && line.startsWith("@")) {
|
|
1100
|
+
rl.prompt();
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
if (!query)
|
|
1104
|
+
query = line;
|
|
1105
|
+
// Inline URL detection — extract URL from query, fetch as context
|
|
1106
|
+
if (!contextText) {
|
|
1107
|
+
const urlInline = query.match(/(https?:\/\/\S+)/);
|
|
1108
|
+
if (urlInline) {
|
|
1109
|
+
const url = urlInline[1];
|
|
1110
|
+
const queryWithoutUrl = query.replace(url, "").trim();
|
|
1111
|
+
console.log(` ${c.dim}Fetching ${url}...${c.reset}`);
|
|
1112
|
+
try {
|
|
1113
|
+
const resp = await fetch(url);
|
|
1114
|
+
if (!resp.ok)
|
|
1115
|
+
throw new Error(`${resp.status} ${resp.statusText}`);
|
|
1116
|
+
contextText = await resp.text();
|
|
1117
|
+
contextSource = url;
|
|
1118
|
+
const lines = contextText.split("\n").length;
|
|
1119
|
+
console.log(` ${c.green}✓${c.reset} Loaded ${c.bold}${contextText.length.toLocaleString()}${c.reset} chars (${lines.toLocaleString()} lines) from URL`);
|
|
1120
|
+
if (queryWithoutUrl) {
|
|
1121
|
+
query = queryWithoutUrl;
|
|
1122
|
+
}
|
|
1123
|
+
else {
|
|
1124
|
+
// URL only, no query — prompt for one
|
|
1125
|
+
printStatusLine();
|
|
1126
|
+
console.log(`\n ${c.dim}Now type your query...${c.reset}\n`);
|
|
1127
|
+
rl.prompt();
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1084
1130
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
console.log(`\n ${c.dim}Now type your query...${c.reset}\n`);
|
|
1089
|
-
rl.prompt();
|
|
1090
|
-
return;
|
|
1131
|
+
catch (err) {
|
|
1132
|
+
console.log(` ${c.red}Failed to fetch URL: ${err.message}${c.reset}`);
|
|
1133
|
+
console.log(` ${c.dim}Running query as-is...${c.reset}`);
|
|
1091
1134
|
}
|
|
1092
1135
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1136
|
+
}
|
|
1137
|
+
// Auto-detect file paths
|
|
1138
|
+
if (!contextText) {
|
|
1139
|
+
const { filePath, query: extractedQuery } = extractFilePath(query);
|
|
1140
|
+
if (filePath) {
|
|
1141
|
+
contextText = fs.readFileSync(filePath, "utf-8");
|
|
1142
|
+
contextSource = path.basename(filePath);
|
|
1143
|
+
const lines = contextText.split("\n").length;
|
|
1144
|
+
console.log(` ${c.green}✓${c.reset} Loaded ${c.bold}${contextText.length.toLocaleString()}${c.reset} chars (${lines} lines) from ${c.underline}${filePath}${c.reset}`);
|
|
1145
|
+
query = extractedQuery || query;
|
|
1096
1146
|
}
|
|
1097
1147
|
}
|
|
1148
|
+
// Run query
|
|
1149
|
+
await runQuery(query);
|
|
1150
|
+
printStatusLine();
|
|
1151
|
+
console.log();
|
|
1152
|
+
rl.prompt();
|
|
1098
1153
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
if (filePath) {
|
|
1103
|
-
contextText = fs.readFileSync(filePath, "utf-8");
|
|
1104
|
-
contextSource = path.basename(filePath);
|
|
1105
|
-
const lines = contextText.split("\n").length;
|
|
1106
|
-
console.log(` ${c.green}✓${c.reset} Loaded ${c.bold}${contextText.length.toLocaleString()}${c.reset} chars (${lines} lines) from ${c.underline}${filePath}${c.reset}`);
|
|
1107
|
-
query = extractedQuery || query;
|
|
1108
|
-
}
|
|
1154
|
+
catch (err) {
|
|
1155
|
+
console.log(`\n ${c.red}Error: ${err?.message || err}${c.reset}\n`);
|
|
1156
|
+
rl.prompt();
|
|
1109
1157
|
}
|
|
1110
|
-
// Run query
|
|
1111
|
-
await runQuery(query);
|
|
1112
|
-
printStatusLine();
|
|
1113
|
-
console.log();
|
|
1114
|
-
rl.prompt();
|
|
1115
1158
|
});
|
|
1116
1159
|
// Ctrl+C: abort running query, or double-tap to exit
|
|
1117
1160
|
let lastSigint = 0;
|