rscot-agent 1.2.2 → 1.7.47
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/agent-runtime.cjs +3824 -0
- package/dist/agent-runtime.js +3833 -0
- package/dist/index.js +412 -65
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import kleur2 from "kleur";
|
|
|
8
8
|
// src/install.ts
|
|
9
9
|
import prompts from "prompts";
|
|
10
10
|
import ora from "ora";
|
|
11
|
+
import { existsSync as existsSync4, statSync as statSync2 } from "node:fs";
|
|
11
12
|
|
|
12
13
|
// src/ui.ts
|
|
13
14
|
import kleur from "kleur";
|
|
@@ -116,7 +117,7 @@ function exampleGrid(examples) {
|
|
|
116
117
|
// src/checks.ts
|
|
117
118
|
import { execFile } from "node:child_process";
|
|
118
119
|
import { promisify } from "node:util";
|
|
119
|
-
import { existsSync, mkdirSync, accessSync, constants } from "node:fs";
|
|
120
|
+
import { existsSync, mkdirSync, accessSync, constants, readdirSync, statSync } from "node:fs";
|
|
120
121
|
import { homedir } from "node:os";
|
|
121
122
|
import { join } from "node:path";
|
|
122
123
|
var pexec = promisify(execFile);
|
|
@@ -126,8 +127,8 @@ async function tryRun(cmd, args) {
|
|
|
126
127
|
let resolvedPath;
|
|
127
128
|
try {
|
|
128
129
|
const which = process.platform === "win32" ? "where" : "which";
|
|
129
|
-
const
|
|
130
|
-
resolvedPath =
|
|
130
|
+
const w2 = await pexec(which, [cmd], { timeout: 2e3, windowsHide: true });
|
|
131
|
+
resolvedPath = w2.stdout.split(/\r?\n/)[0]?.trim();
|
|
131
132
|
} catch {
|
|
132
133
|
}
|
|
133
134
|
return { out: stdout.trim(), path: resolvedPath };
|
|
@@ -228,18 +229,41 @@ async function checkProviderKeys() {
|
|
|
228
229
|
"DEEPSEEK_API_KEY",
|
|
229
230
|
"OPENROUTER_API_KEY"
|
|
230
231
|
];
|
|
231
|
-
const
|
|
232
|
-
|
|
232
|
+
const envFound = envKeys.filter((k) => (process.env[k] ?? "").length > 8);
|
|
233
|
+
const keysDir2 = join(homedir(), ".rscot", "keys");
|
|
234
|
+
const fileFound = [];
|
|
235
|
+
try {
|
|
236
|
+
if (existsSync(keysDir2)) {
|
|
237
|
+
for (const f of readdirSync(keysDir2)) {
|
|
238
|
+
if (!f.endsWith(".key"))
|
|
239
|
+
continue;
|
|
240
|
+
try {
|
|
241
|
+
if (statSync(join(keysDir2, f)).size > 8) {
|
|
242
|
+
fileFound.push(f.replace(/\.key$/, ""));
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
const total = envFound.length + fileFound.length;
|
|
251
|
+
if (total > 0) {
|
|
252
|
+
const parts = [];
|
|
253
|
+
if (envFound.length)
|
|
254
|
+
parts.push("env: " + envFound.join(", "));
|
|
255
|
+
if (fileFound.length)
|
|
256
|
+
parts.push("~/.rscot/keys/: " + fileFound.join(", "));
|
|
233
257
|
return {
|
|
234
258
|
name: "LLM provider key",
|
|
235
259
|
ok: true,
|
|
236
|
-
detail:
|
|
260
|
+
detail: parts.join(" \xB7 ")
|
|
237
261
|
};
|
|
238
262
|
}
|
|
239
263
|
return {
|
|
240
264
|
name: "LLM provider key",
|
|
241
265
|
ok: false,
|
|
242
|
-
detail: "no ANTHROPIC_API_KEY \xB7 OPENAI_API_KEY \xB7 DEEPSEEK_API_KEY \xB7 OPENROUTER_API_KEY in env"
|
|
266
|
+
detail: "no ANTHROPIC_API_KEY \xB7 OPENAI_API_KEY \xB7 DEEPSEEK_API_KEY \xB7 OPENROUTER_API_KEY in env, no key file under ~/.rscot/keys/"
|
|
243
267
|
};
|
|
244
268
|
}
|
|
245
269
|
function ensureConfigDir() {
|
|
@@ -358,6 +382,22 @@ var providers = [
|
|
|
358
382
|
keyMinLen: 24,
|
|
359
383
|
envVar: "OPENROUTER_API_KEY"
|
|
360
384
|
},
|
|
385
|
+
{
|
|
386
|
+
id: "kimi",
|
|
387
|
+
display: "Kimi (Moonshot)",
|
|
388
|
+
blurb: "Moonshot Kimi K2 \xB7 long-context CN flagship \xB7 OpenAI-compatible",
|
|
389
|
+
needsKey: true,
|
|
390
|
+
baseUrl: "https://api.moonshot.cn/v1",
|
|
391
|
+
defaultModel: "kimi-k2-turbo",
|
|
392
|
+
models: [
|
|
393
|
+
{ id: "kimi-k2-turbo", label: "kimi-k2-turbo", note: "default \xB7 fast \xB7 256K ctx", priceIn: 0.6, priceOut: 2.4 },
|
|
394
|
+
{ id: "kimi-k2", label: "kimi-k2", note: "flagship \xB7 reasoning", priceIn: 1.2, priceOut: 4.8 }
|
|
395
|
+
],
|
|
396
|
+
keyHelpUrl: "https://platform.moonshot.cn/console/api-keys",
|
|
397
|
+
keyPrefix: "sk-",
|
|
398
|
+
keyMinLen: 24,
|
|
399
|
+
envVar: "MOONSHOT_API_KEY"
|
|
400
|
+
},
|
|
361
401
|
{
|
|
362
402
|
id: "groq",
|
|
363
403
|
display: "Groq",
|
|
@@ -445,7 +485,7 @@ var providers = [
|
|
|
445
485
|
{
|
|
446
486
|
id: "custom",
|
|
447
487
|
display: "OpenAI-compatible (custom)",
|
|
448
|
-
blurb: "vLLM / Cerebras / Fireworks / self-hosted endpoint",
|
|
488
|
+
blurb: "OpenToken / vLLM / Cerebras / Fireworks / self-hosted endpoint",
|
|
449
489
|
needsKey: false,
|
|
450
490
|
baseUrl: "",
|
|
451
491
|
defaultModel: "",
|
|
@@ -519,10 +559,284 @@ function saveConfig(opts) {
|
|
|
519
559
|
}
|
|
520
560
|
return { configPath: path, keyPath: kp };
|
|
521
561
|
}
|
|
562
|
+
function loadConfig() {
|
|
563
|
+
const p = configPath();
|
|
564
|
+
if (!existsSync2(p))
|
|
565
|
+
return null;
|
|
566
|
+
try {
|
|
567
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
568
|
+
} catch {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
522
572
|
|
|
523
|
-
// src/
|
|
524
|
-
|
|
573
|
+
// src/repl.ts
|
|
574
|
+
import readline from "node:readline";
|
|
575
|
+
import { homedir as homedir4 } from "node:os";
|
|
576
|
+
|
|
577
|
+
// src/agentRuntime.ts
|
|
578
|
+
import { spawn } from "node:child_process";
|
|
579
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
|
|
580
|
+
import { homedir as homedir3 } from "node:os";
|
|
581
|
+
import { dirname as dirname2, join as join3, resolve } from "node:path";
|
|
582
|
+
import { fileURLToPath } from "node:url";
|
|
583
|
+
import { createRequire } from "node:module";
|
|
584
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
585
|
+
var __dirname = dirname2(__filename);
|
|
586
|
+
var req = createRequire(import.meta.url);
|
|
587
|
+
function findAgentRuntime() {
|
|
588
|
+
const envPath = process.env.RSCOT_AGENT_RUNTIME;
|
|
589
|
+
if (envPath && existsSync3(envPath))
|
|
590
|
+
return envPath;
|
|
591
|
+
const bundled = join3(__dirname, "agent-runtime.cjs");
|
|
592
|
+
if (existsSync3(bundled))
|
|
593
|
+
return bundled;
|
|
594
|
+
const monoDev = resolve(__dirname, "..", "..", "vscode-extension", "dist", "cli.js");
|
|
595
|
+
if (existsSync3(monoDev))
|
|
596
|
+
return monoDev;
|
|
597
|
+
try {
|
|
598
|
+
return req.resolve("code-workspace-agent/dist/cli.js");
|
|
599
|
+
} catch {
|
|
600
|
+
}
|
|
601
|
+
throw new Error(
|
|
602
|
+
"Rscot agent runtime (cli.js) not found. Set RSCOT_AGENT_RUNTIME to the cli.js path or install the desktop app which ships it."
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
function readApiKey(cfg) {
|
|
606
|
+
const kp = keyPath(cfg.provider);
|
|
607
|
+
if (!existsSync3(kp))
|
|
608
|
+
return "";
|
|
609
|
+
try {
|
|
610
|
+
return readFileSync2(kp, "utf8").trim();
|
|
611
|
+
} catch {
|
|
612
|
+
return "";
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function buildAgentArgs(cfg, opts) {
|
|
616
|
+
const args = [
|
|
617
|
+
"--workspace",
|
|
618
|
+
opts.workspace,
|
|
619
|
+
"--backend-url",
|
|
620
|
+
opts.backendUrl,
|
|
621
|
+
"--provider",
|
|
622
|
+
cfg.provider,
|
|
623
|
+
"--stream"
|
|
624
|
+
];
|
|
625
|
+
if (cfg.model)
|
|
626
|
+
args.push("--model", cfg.model);
|
|
627
|
+
if (opts.resume)
|
|
628
|
+
args.push("--resume");
|
|
629
|
+
if (opts.quiet)
|
|
630
|
+
args.push("--quiet");
|
|
631
|
+
if (opts.extraArgs)
|
|
632
|
+
args.push(...opts.extraArgs);
|
|
633
|
+
if (opts.prompt !== void 0)
|
|
634
|
+
args.push(opts.prompt);
|
|
635
|
+
return args;
|
|
636
|
+
}
|
|
637
|
+
async function preflightBackend(backendUrl2) {
|
|
638
|
+
try {
|
|
639
|
+
const res = await fetch(backendUrl2 + "/health", {
|
|
640
|
+
signal: AbortSignal.timeout(1500)
|
|
641
|
+
});
|
|
642
|
+
if (res.ok)
|
|
643
|
+
return { ok: true };
|
|
644
|
+
return { ok: false, detail: "HTTP " + res.status };
|
|
645
|
+
} catch (err) {
|
|
646
|
+
return { ok: false, detail: err instanceof Error ? err.message : String(err) };
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function readCwaAdminToken() {
|
|
650
|
+
const path = join3(homedir3(), ".rscot", "cwa", "token");
|
|
651
|
+
if (!existsSync3(path))
|
|
652
|
+
return null;
|
|
653
|
+
try {
|
|
654
|
+
const t = readFileSync2(path, "utf8").trim();
|
|
655
|
+
return t.length > 0 ? t : null;
|
|
656
|
+
} catch {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function runAgent(opts) {
|
|
661
|
+
const cfg = loadConfig();
|
|
662
|
+
if (!cfg)
|
|
663
|
+
return Promise.resolve(1);
|
|
664
|
+
const runtime = findAgentRuntime();
|
|
665
|
+
const args = buildAgentArgs(cfg, opts);
|
|
666
|
+
const env = { ...process.env };
|
|
667
|
+
const apiKey = readApiKey(cfg);
|
|
668
|
+
if (apiKey)
|
|
669
|
+
env.CWA_API_KEY = apiKey;
|
|
670
|
+
const cwaToken = readCwaAdminToken();
|
|
671
|
+
if (cwaToken)
|
|
672
|
+
env.CWA_AUTH_BEARER = cwaToken;
|
|
673
|
+
return new Promise((resolve2) => {
|
|
674
|
+
const child = spawn(process.execPath, [runtime, ...args], {
|
|
675
|
+
stdio: "inherit",
|
|
676
|
+
env
|
|
677
|
+
});
|
|
678
|
+
const onSigint = () => {
|
|
679
|
+
try {
|
|
680
|
+
child.kill("SIGINT");
|
|
681
|
+
} catch {
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
process.on("SIGINT", onSigint);
|
|
685
|
+
child.on("exit", (code) => {
|
|
686
|
+
process.off("SIGINT", onSigint);
|
|
687
|
+
resolve2(code ?? 0);
|
|
688
|
+
});
|
|
689
|
+
child.on("error", (err) => {
|
|
690
|
+
process.off("SIGINT", onSigint);
|
|
691
|
+
process.stderr.write("\nrscot: agent runtime failed to start \u2014 " + err.message + "\n");
|
|
692
|
+
resolve2(1);
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// src/repl.ts
|
|
698
|
+
var VERSION = "1.3.0";
|
|
525
699
|
var BUILD = "a7f31c2";
|
|
700
|
+
function homeShort(p) {
|
|
701
|
+
const h = homedir4();
|
|
702
|
+
return p.startsWith(h) ? "~" + p.slice(h.length) : p;
|
|
703
|
+
}
|
|
704
|
+
function w(s) {
|
|
705
|
+
process.stdout.write(s);
|
|
706
|
+
}
|
|
707
|
+
function printReadyBanner(cfg, workspace, backendUrl2) {
|
|
708
|
+
w("\n" + brandHeader(VERSION, BUILD) + "\n\n");
|
|
709
|
+
w(
|
|
710
|
+
" " + palette.success("\u25CF") + " " + palette.dim("ready ") + palette.cyan(cfg.provider + "/" + cfg.model) + " " + palette.dim("\xB7 ") + palette.cmd(homeShort(workspace)) + "\n"
|
|
711
|
+
);
|
|
712
|
+
w(
|
|
713
|
+
" " + palette.dim(" backend ") + palette.cyan(backendUrl2) + "\n"
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
function printReplHelp() {
|
|
717
|
+
w("\n");
|
|
718
|
+
w(" " + palette.bold("REPL commands") + "\n");
|
|
719
|
+
w(" " + palette.cyan("<any text>") + palette.dim(" send as prompt to the agent") + "\n");
|
|
720
|
+
w(" " + palette.cyan("/help") + palette.dim(" show this help") + "\n");
|
|
721
|
+
w(" " + palette.cyan("/reset") + palette.dim(" start a fresh conversation (drop --resume)") + "\n");
|
|
722
|
+
w(" " + palette.cyan("/cd <dir>") + palette.dim(" change working directory") + "\n");
|
|
723
|
+
w(" " + palette.cyan("exit") + palette.dim(" | ") + palette.cyan("quit") + palette.dim(" | Ctrl+D leave REPL") + "\n");
|
|
724
|
+
w(" " + palette.dim("(during a turn, Ctrl+C forwards to agent and cancels it)") + "\n\n");
|
|
725
|
+
}
|
|
726
|
+
function promptLine(promptStr) {
|
|
727
|
+
return new Promise((resolve2) => {
|
|
728
|
+
const rl = readline.createInterface({
|
|
729
|
+
input: process.stdin,
|
|
730
|
+
output: process.stdout,
|
|
731
|
+
terminal: true,
|
|
732
|
+
historySize: 500
|
|
733
|
+
});
|
|
734
|
+
rl.setPrompt(promptStr);
|
|
735
|
+
rl.prompt();
|
|
736
|
+
let settled = false;
|
|
737
|
+
const done = (v) => {
|
|
738
|
+
if (settled)
|
|
739
|
+
return;
|
|
740
|
+
settled = true;
|
|
741
|
+
try {
|
|
742
|
+
rl.close();
|
|
743
|
+
} catch {
|
|
744
|
+
}
|
|
745
|
+
resolve2(v);
|
|
746
|
+
};
|
|
747
|
+
rl.once("line", (line) => done(line));
|
|
748
|
+
rl.once("close", () => done(null));
|
|
749
|
+
rl.on("SIGINT", () => done(null));
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
async function runOneShot(prompt, workspace, backendUrl2) {
|
|
753
|
+
const cfg = loadConfig();
|
|
754
|
+
if (!cfg) {
|
|
755
|
+
process.stderr.write(palette.error("rscot: not installed \u2014 run `rscot` first\n"));
|
|
756
|
+
return 1;
|
|
757
|
+
}
|
|
758
|
+
const pf = await preflightBackend(backendUrl2);
|
|
759
|
+
if (!pf.ok) {
|
|
760
|
+
process.stderr.write(
|
|
761
|
+
palette.error("rscot: backend unreachable at " + backendUrl2) + palette.dim(" (" + (pf.detail ?? "no detail") + ")\n") + palette.dim(" start it with `docker compose up -d` or set RSCOT_BACKEND_URL\n")
|
|
762
|
+
);
|
|
763
|
+
return 1;
|
|
764
|
+
}
|
|
765
|
+
return runAgent({ workspace, backendUrl: backendUrl2, prompt });
|
|
766
|
+
}
|
|
767
|
+
async function runRepl(workspace, backendUrl2) {
|
|
768
|
+
const cfg = loadConfig();
|
|
769
|
+
if (!cfg) {
|
|
770
|
+
process.stderr.write(palette.error("rscot: not installed \u2014 run `rscot` first\n"));
|
|
771
|
+
return 1;
|
|
772
|
+
}
|
|
773
|
+
const pf = await preflightBackend(backendUrl2);
|
|
774
|
+
printReadyBanner(cfg, workspace, backendUrl2);
|
|
775
|
+
if (!pf.ok) {
|
|
776
|
+
w(
|
|
777
|
+
" " + palette.error("\u2717") + palette.dim(" backend ") + palette.error("unreachable") + palette.dim(" (" + (pf.detail ?? "no detail") + ")\n")
|
|
778
|
+
);
|
|
779
|
+
w(palette.dim(" start it with `docker compose up -d` or set RSCOT_BACKEND_URL\n\n"));
|
|
780
|
+
} else {
|
|
781
|
+
w("\n");
|
|
782
|
+
}
|
|
783
|
+
w(" " + palette.dim("hint type a prompt \xB7 /help for commands \xB7 empty line or Ctrl+D quits\n\n"));
|
|
784
|
+
let currentWorkspace = workspace;
|
|
785
|
+
let turnCount = 0;
|
|
786
|
+
let resumeNext = false;
|
|
787
|
+
while (true) {
|
|
788
|
+
const cwdShort = homeShort(currentWorkspace);
|
|
789
|
+
const promptStr = palette.dim(cwdShort + " ") + palette.prompt("\u258C ");
|
|
790
|
+
const raw = await promptLine(promptStr);
|
|
791
|
+
if (raw === null)
|
|
792
|
+
break;
|
|
793
|
+
const line = raw.trim();
|
|
794
|
+
if (line === "" || line === "exit" || line === "quit")
|
|
795
|
+
break;
|
|
796
|
+
if (line === "/help") {
|
|
797
|
+
printReplHelp();
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (line === "/reset") {
|
|
801
|
+
resumeNext = false;
|
|
802
|
+
turnCount = 0;
|
|
803
|
+
w(palette.dim(" \xB7 conversation reset \xB7 next turn starts a fresh session\n\n"));
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (line.startsWith("/cd ")) {
|
|
807
|
+
const target = line.slice(4).trim();
|
|
808
|
+
try {
|
|
809
|
+
process.chdir(target);
|
|
810
|
+
currentWorkspace = process.cwd();
|
|
811
|
+
w(palette.dim(" \xB7 cwd = ") + palette.cyan(homeShort(currentWorkspace)) + "\n\n");
|
|
812
|
+
} catch (err) {
|
|
813
|
+
w(palette.error(" \xB7 cd failed: " + (err instanceof Error ? err.message : String(err)) + "\n\n"));
|
|
814
|
+
}
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
const rc = await runAgent({
|
|
818
|
+
workspace: currentWorkspace,
|
|
819
|
+
backendUrl: backendUrl2,
|
|
820
|
+
prompt: line,
|
|
821
|
+
resume: resumeNext
|
|
822
|
+
});
|
|
823
|
+
turnCount++;
|
|
824
|
+
resumeNext = true;
|
|
825
|
+
if (rc !== 0) {
|
|
826
|
+
w("\n" + palette.warn("!") + " agent exited with code " + rc + "\n");
|
|
827
|
+
}
|
|
828
|
+
w("\n");
|
|
829
|
+
}
|
|
830
|
+
w("\n" + palette.dim(" bye \xB7 ") + palette.dim(turnCount + " turn" + (turnCount === 1 ? "" : "s") + " this session\n\n"));
|
|
831
|
+
return 0;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/install.ts
|
|
835
|
+
var VERSION2 = "1.3.0";
|
|
836
|
+
var BUILD2 = "a7f31c2";
|
|
837
|
+
function backendUrl() {
|
|
838
|
+
return process.env.RSCOT_BACKEND_URL || "http://localhost:8010";
|
|
839
|
+
}
|
|
526
840
|
function nl(n = 1) {
|
|
527
841
|
process.stdout.write("\n".repeat(n));
|
|
528
842
|
}
|
|
@@ -535,7 +849,7 @@ function sleep(ms) {
|
|
|
535
849
|
async function runEnvironmentProbe() {
|
|
536
850
|
println(palette.prompt("$") + " " + palette.cmd("npx rscot"));
|
|
537
851
|
nl();
|
|
538
|
-
println(brandHeader(
|
|
852
|
+
println(brandHeader(VERSION2, BUILD2));
|
|
539
853
|
nl();
|
|
540
854
|
println(sectionTitle("Checking your environment\u2026"));
|
|
541
855
|
nl();
|
|
@@ -681,7 +995,7 @@ async function runInstallPhases(provider, apiKey) {
|
|
|
681
995
|
);
|
|
682
996
|
}
|
|
683
997
|
nl();
|
|
684
|
-
println(sectionTitle("Installing Rscot Agent v" +
|
|
998
|
+
println(sectionTitle("Installing Rscot Agent v" + VERSION2));
|
|
685
999
|
println(rule());
|
|
686
1000
|
nl();
|
|
687
1001
|
const phases = [
|
|
@@ -714,7 +1028,7 @@ async function runInstallPhases(provider, apiKey) {
|
|
|
714
1028
|
model: provider.defaultModel,
|
|
715
1029
|
apiKey: provider.needsKey ? apiKey : void 0,
|
|
716
1030
|
apiKeyEnvVar: provider.envVar,
|
|
717
|
-
version:
|
|
1031
|
+
version: VERSION2
|
|
718
1032
|
});
|
|
719
1033
|
const elapsedMs = Date.now() - startedAt;
|
|
720
1034
|
const ss = Math.round(elapsedMs / 1e3);
|
|
@@ -731,7 +1045,7 @@ async function runInstallPhases(provider, apiKey) {
|
|
|
731
1045
|
function printSuccess(provider) {
|
|
732
1046
|
println(
|
|
733
1047
|
successBanner([
|
|
734
|
-
"Installed Rscot Agent v" +
|
|
1048
|
+
"Installed Rscot Agent v" + VERSION2,
|
|
735
1049
|
"Provider: " + provider.display + (provider.needsKey ? " (key stored)" : "")
|
|
736
1050
|
])
|
|
737
1051
|
);
|
|
@@ -764,8 +1078,32 @@ function printSuccess(provider) {
|
|
|
764
1078
|
);
|
|
765
1079
|
nl();
|
|
766
1080
|
}
|
|
1081
|
+
function existingKeyForConfig(cfg) {
|
|
1082
|
+
try {
|
|
1083
|
+
const kp = keyPath(cfg.provider);
|
|
1084
|
+
if (existsSync4(kp) && statSync2(kp).size > 8)
|
|
1085
|
+
return true;
|
|
1086
|
+
} catch {
|
|
1087
|
+
}
|
|
1088
|
+
const envVar = cfg.apiKeyEnvVar;
|
|
1089
|
+
if (envVar && (process.env[envVar] ?? "").length > 8)
|
|
1090
|
+
return true;
|
|
1091
|
+
return false;
|
|
1092
|
+
}
|
|
1093
|
+
async function runAlreadyInstalled(cfg, positional) {
|
|
1094
|
+
const workspace = process.cwd();
|
|
1095
|
+
const url = backendUrl();
|
|
1096
|
+
if (positional)
|
|
1097
|
+
return runOneShot(positional, workspace, url);
|
|
1098
|
+
return runRepl(workspace, url);
|
|
1099
|
+
}
|
|
767
1100
|
async function runInstall(opts = {}) {
|
|
768
1101
|
try {
|
|
1102
|
+
const existing = loadConfig();
|
|
1103
|
+
if (existing && existingKeyForConfig(existing)) {
|
|
1104
|
+
const positional2 = process.argv.slice(2).find((a) => !a.startsWith("-") && !["login", "logout", "whoami", "providers", "models", "keys", "config"].includes(a));
|
|
1105
|
+
return runAlreadyInstalled(existing, positional2 ?? null);
|
|
1106
|
+
}
|
|
769
1107
|
const results = await runEnvironmentProbe();
|
|
770
1108
|
printEnvironmentReport(results);
|
|
771
1109
|
if (opts.noninteractive) {
|
|
@@ -803,7 +1141,12 @@ async function runInstall(opts = {}) {
|
|
|
803
1141
|
}
|
|
804
1142
|
await runInstallPhases(provider, apiKey);
|
|
805
1143
|
printSuccess(provider);
|
|
806
|
-
|
|
1144
|
+
const positional = process.argv.slice(2).find((a) => !a.startsWith("-") && !["login", "logout", "whoami", "providers", "models", "keys", "config"].includes(a));
|
|
1145
|
+
const workspace = process.cwd();
|
|
1146
|
+
const url = backendUrl();
|
|
1147
|
+
if (positional)
|
|
1148
|
+
return runOneShot(positional, workspace, url);
|
|
1149
|
+
return runRepl(workspace, url);
|
|
807
1150
|
} catch (err) {
|
|
808
1151
|
println(palette.error("\n install failed: " + (err instanceof Error ? err.message : String(err))));
|
|
809
1152
|
return 1;
|
|
@@ -811,22 +1154,22 @@ async function runInstall(opts = {}) {
|
|
|
811
1154
|
}
|
|
812
1155
|
|
|
813
1156
|
// src/commands/catalog.ts
|
|
814
|
-
import { existsSync as
|
|
815
|
-
import { join as
|
|
1157
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1158
|
+
import { join as join4 } from "node:path";
|
|
816
1159
|
var CACHE_FILE = "providers.cache.json";
|
|
817
1160
|
var TTL_HOURS = 24;
|
|
818
1161
|
function cachePath() {
|
|
819
1162
|
const dir = configRoot();
|
|
820
|
-
if (!
|
|
1163
|
+
if (!existsSync5(dir))
|
|
821
1164
|
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
822
|
-
return
|
|
1165
|
+
return join4(dir, CACHE_FILE);
|
|
823
1166
|
}
|
|
824
1167
|
function readCache() {
|
|
825
1168
|
try {
|
|
826
1169
|
const p = cachePath();
|
|
827
|
-
if (!
|
|
1170
|
+
if (!existsSync5(p))
|
|
828
1171
|
return null;
|
|
829
|
-
return JSON.parse(
|
|
1172
|
+
return JSON.parse(readFileSync3(p, "utf8"));
|
|
830
1173
|
} catch {
|
|
831
1174
|
return null;
|
|
832
1175
|
}
|
|
@@ -839,9 +1182,9 @@ function isFresh(c) {
|
|
|
839
1182
|
return false;
|
|
840
1183
|
return Date.now() - t < TTL_HOURS * 3600 * 1e3;
|
|
841
1184
|
}
|
|
842
|
-
async function fetchBackend(
|
|
1185
|
+
async function fetchBackend(backendUrl2) {
|
|
843
1186
|
try {
|
|
844
|
-
const res = await fetch(`${
|
|
1187
|
+
const res = await fetch(`${backendUrl2.replace(/\/+$/, "")}/api/providers`);
|
|
845
1188
|
if (!res.ok)
|
|
846
1189
|
return null;
|
|
847
1190
|
return await res.json();
|
|
@@ -871,13 +1214,13 @@ function fromBackend(p) {
|
|
|
871
1214
|
};
|
|
872
1215
|
}
|
|
873
1216
|
async function loadCatalog(opts = {}) {
|
|
874
|
-
const
|
|
1217
|
+
const backendUrl2 = opts.backendUrl || process.env.RSCOT_BACKEND_URL || "https://learningfind.com/api";
|
|
875
1218
|
if (!opts.force) {
|
|
876
1219
|
const cached2 = readCache();
|
|
877
1220
|
if (isFresh(cached2))
|
|
878
1221
|
return cached2.payload.providers.map(fromBackend);
|
|
879
1222
|
}
|
|
880
|
-
const payload = await fetchBackend(
|
|
1223
|
+
const payload = await fetchBackend(backendUrl2);
|
|
881
1224
|
if (payload) {
|
|
882
1225
|
try {
|
|
883
1226
|
writeFileSync2(
|
|
@@ -896,16 +1239,16 @@ async function loadCatalog(opts = {}) {
|
|
|
896
1239
|
}
|
|
897
1240
|
|
|
898
1241
|
// src/commands/keys.ts
|
|
899
|
-
import { existsSync as
|
|
900
|
-
import { join as
|
|
1242
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, rmSync, readdirSync as readdirSync2, chmodSync as chmodSync2 } from "node:fs";
|
|
1243
|
+
import { join as join5 } from "node:path";
|
|
901
1244
|
function keysDir() {
|
|
902
|
-
const d =
|
|
903
|
-
if (!
|
|
1245
|
+
const d = join5(configRoot(), "keys");
|
|
1246
|
+
if (!existsSync6(d))
|
|
904
1247
|
mkdirSync4(d, { recursive: true, mode: 448 });
|
|
905
1248
|
return d;
|
|
906
1249
|
}
|
|
907
1250
|
function keyFile(providerId) {
|
|
908
|
-
return
|
|
1251
|
+
return join5(keysDir(), `${providerId}.key`);
|
|
909
1252
|
}
|
|
910
1253
|
function setKey(providerId, value) {
|
|
911
1254
|
const path = keyFile(providerId);
|
|
@@ -918,17 +1261,17 @@ function setKey(providerId, value) {
|
|
|
918
1261
|
}
|
|
919
1262
|
function getKey(providerId) {
|
|
920
1263
|
const path = keyFile(providerId);
|
|
921
|
-
if (!
|
|
1264
|
+
if (!existsSync6(path))
|
|
922
1265
|
return "";
|
|
923
1266
|
try {
|
|
924
|
-
return
|
|
1267
|
+
return readFileSync4(path, "utf8").trim();
|
|
925
1268
|
} catch {
|
|
926
1269
|
return "";
|
|
927
1270
|
}
|
|
928
1271
|
}
|
|
929
1272
|
function removeKey(providerId) {
|
|
930
1273
|
const path = keyFile(providerId);
|
|
931
|
-
if (!
|
|
1274
|
+
if (!existsSync6(path))
|
|
932
1275
|
return false;
|
|
933
1276
|
rmSync(path);
|
|
934
1277
|
return true;
|
|
@@ -936,7 +1279,7 @@ function removeKey(providerId) {
|
|
|
936
1279
|
function listKeys() {
|
|
937
1280
|
const d = keysDir();
|
|
938
1281
|
try {
|
|
939
|
-
return
|
|
1282
|
+
return readdirSync2(d).filter((f) => f.endsWith(".key")).map((f) => f.replace(/\.key$/, "")).sort();
|
|
940
1283
|
} catch {
|
|
941
1284
|
return [];
|
|
942
1285
|
}
|
|
@@ -953,12 +1296,12 @@ function maskKey(raw) {
|
|
|
953
1296
|
return "***" + raw.slice(-2);
|
|
954
1297
|
return raw.slice(0, 4) + "\u2022\u2022\u2022\u2022\u2022" + raw.slice(-4);
|
|
955
1298
|
}
|
|
956
|
-
function pad(s,
|
|
957
|
-
return s.length >=
|
|
1299
|
+
function pad(s, w2) {
|
|
1300
|
+
return s.length >= w2 ? s : s + " ".repeat(w2 - s.length);
|
|
958
1301
|
}
|
|
959
1302
|
async function readStdinLine(promptText) {
|
|
960
1303
|
process.stdout.write(palette.prompt(promptText));
|
|
961
|
-
return new Promise((
|
|
1304
|
+
return new Promise((resolve2) => {
|
|
962
1305
|
const chunks = [];
|
|
963
1306
|
const onData = (b) => {
|
|
964
1307
|
chunks.push(b);
|
|
@@ -970,7 +1313,7 @@ async function readStdinLine(promptText) {
|
|
|
970
1313
|
process.stdin.pause();
|
|
971
1314
|
} catch {
|
|
972
1315
|
}
|
|
973
|
-
|
|
1316
|
+
resolve2(buf.slice(0, nl2).replace(/\r$/, ""));
|
|
974
1317
|
}
|
|
975
1318
|
};
|
|
976
1319
|
process.stdin.on("data", onData);
|
|
@@ -1177,11 +1520,11 @@ async function cmdKeysRemove(providerId) {
|
|
|
1177
1520
|
}
|
|
1178
1521
|
|
|
1179
1522
|
// src/auth.ts
|
|
1180
|
-
import { existsSync as
|
|
1181
|
-
import { join as
|
|
1523
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, rmSync as rmSync2, chmodSync as chmodSync3 } from "node:fs";
|
|
1524
|
+
import { join as join6 } from "node:path";
|
|
1182
1525
|
import * as crypto from "node:crypto";
|
|
1183
1526
|
import * as http from "node:http";
|
|
1184
|
-
import { spawn } from "node:child_process";
|
|
1527
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1185
1528
|
var AUTH_BASE = "https://auth.learningfind.com";
|
|
1186
1529
|
var PROFILE_URL = "https://learningfind.com/api/me/profile";
|
|
1187
1530
|
var PORT_LO = 49152;
|
|
@@ -1191,27 +1534,27 @@ var LOOPBACK_TIMEOUT_MS = 18e4;
|
|
|
1191
1534
|
var ACCESS_LEEWAY_S = 30;
|
|
1192
1535
|
function authDir() {
|
|
1193
1536
|
const d = configRoot();
|
|
1194
|
-
if (!
|
|
1537
|
+
if (!existsSync7(d))
|
|
1195
1538
|
mkdirSync5(d, { recursive: true, mode: 448 });
|
|
1196
1539
|
return d;
|
|
1197
1540
|
}
|
|
1198
1541
|
function pathAccess() {
|
|
1199
|
-
return
|
|
1542
|
+
return join6(authDir(), "access");
|
|
1200
1543
|
}
|
|
1201
1544
|
function pathRefresh() {
|
|
1202
|
-
return
|
|
1545
|
+
return join6(authDir(), "refresh");
|
|
1203
1546
|
}
|
|
1204
1547
|
function pathAccessExp() {
|
|
1205
|
-
return
|
|
1548
|
+
return join6(authDir(), "access.exp");
|
|
1206
1549
|
}
|
|
1207
1550
|
function pathDevice() {
|
|
1208
|
-
return
|
|
1551
|
+
return join6(authDir(), "device_id");
|
|
1209
1552
|
}
|
|
1210
1553
|
function pathProfile() {
|
|
1211
|
-
return
|
|
1554
|
+
return join6(authDir(), "profile.json");
|
|
1212
1555
|
}
|
|
1213
1556
|
function pathLocalMode() {
|
|
1214
|
-
return
|
|
1557
|
+
return join6(authDir(), "local-mode");
|
|
1215
1558
|
}
|
|
1216
1559
|
function writeSecret(path, value) {
|
|
1217
1560
|
writeFileSync4(path, value, { mode: 384 });
|
|
@@ -1221,16 +1564,16 @@ function writeSecret(path, value) {
|
|
|
1221
1564
|
}
|
|
1222
1565
|
}
|
|
1223
1566
|
function readMaybe(path) {
|
|
1224
|
-
if (!
|
|
1567
|
+
if (!existsSync7(path))
|
|
1225
1568
|
return null;
|
|
1226
1569
|
try {
|
|
1227
|
-
return
|
|
1570
|
+
return readFileSync5(path, "utf8").trim();
|
|
1228
1571
|
} catch {
|
|
1229
1572
|
return null;
|
|
1230
1573
|
}
|
|
1231
1574
|
}
|
|
1232
1575
|
function rmIf(path) {
|
|
1233
|
-
if (
|
|
1576
|
+
if (existsSync7(path))
|
|
1234
1577
|
try {
|
|
1235
1578
|
rmSync2(path);
|
|
1236
1579
|
} catch {
|
|
@@ -1269,7 +1612,7 @@ function getOrCreateDeviceId() {
|
|
|
1269
1612
|
return id;
|
|
1270
1613
|
}
|
|
1271
1614
|
function isLocalMode() {
|
|
1272
|
-
return
|
|
1615
|
+
return existsSync7(pathLocalMode());
|
|
1273
1616
|
}
|
|
1274
1617
|
function setLocalMode(on) {
|
|
1275
1618
|
if (on)
|
|
@@ -1340,7 +1683,7 @@ async function getAccessToken() {
|
|
|
1340
1683
|
}
|
|
1341
1684
|
}
|
|
1342
1685
|
function bindLoopback() {
|
|
1343
|
-
return new Promise((
|
|
1686
|
+
return new Promise((resolve2, reject) => {
|
|
1344
1687
|
let attempts = 0;
|
|
1345
1688
|
const tryBind = () => {
|
|
1346
1689
|
if (attempts++ >= BIND_ATTEMPTS) {
|
|
@@ -1359,7 +1702,7 @@ function bindLoopback() {
|
|
|
1359
1702
|
server.listen(port, "127.0.0.1", () => {
|
|
1360
1703
|
const addr = server.address();
|
|
1361
1704
|
server.removeAllListeners("error");
|
|
1362
|
-
|
|
1705
|
+
resolve2({ server, port: addr.port });
|
|
1363
1706
|
});
|
|
1364
1707
|
};
|
|
1365
1708
|
tryBind();
|
|
@@ -1367,7 +1710,7 @@ function bindLoopback() {
|
|
|
1367
1710
|
}
|
|
1368
1711
|
var SUCCESS_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>Rscot Agent</title></head><body style="font-family:system-ui,sans-serif;padding:40px;color:#222"><h2>Sign-in complete</h2><p>You can close this window and return to your terminal.</p><script>setTimeout(function(){window.close();},250);</script></body></html>`;
|
|
1369
1712
|
function waitForCallback(server, expectedState) {
|
|
1370
|
-
return new Promise((
|
|
1713
|
+
return new Promise((resolve2, reject) => {
|
|
1371
1714
|
const timer = setTimeout(() => {
|
|
1372
1715
|
try {
|
|
1373
1716
|
server.close();
|
|
@@ -1375,8 +1718,8 @@ function waitForCallback(server, expectedState) {
|
|
|
1375
1718
|
}
|
|
1376
1719
|
reject(new Error("Sign-in timed out after 3 minutes."));
|
|
1377
1720
|
}, LOOPBACK_TIMEOUT_MS);
|
|
1378
|
-
server.on("request", (
|
|
1379
|
-
const url = new URL(
|
|
1721
|
+
server.on("request", (req2, res) => {
|
|
1722
|
+
const url = new URL(req2.url || "/", "http://127.0.0.1");
|
|
1380
1723
|
if (url.pathname !== "/cb") {
|
|
1381
1724
|
res.statusCode = 404;
|
|
1382
1725
|
res.end("not found");
|
|
@@ -1399,7 +1742,7 @@ function waitForCallback(server, expectedState) {
|
|
|
1399
1742
|
return reject(new Error("Auth callback missing code/state."));
|
|
1400
1743
|
if (state !== expectedState)
|
|
1401
1744
|
return reject(new Error("Auth callback state mismatch."));
|
|
1402
|
-
|
|
1745
|
+
resolve2(code);
|
|
1403
1746
|
});
|
|
1404
1747
|
});
|
|
1405
1748
|
}
|
|
@@ -1435,7 +1778,7 @@ function openBrowser(url) {
|
|
|
1435
1778
|
args = [url];
|
|
1436
1779
|
}
|
|
1437
1780
|
try {
|
|
1438
|
-
const child =
|
|
1781
|
+
const child = spawn2(cmd, args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
1439
1782
|
child.unref();
|
|
1440
1783
|
} catch {
|
|
1441
1784
|
}
|
|
@@ -1455,13 +1798,17 @@ async function signIn() {
|
|
|
1455
1798
|
client: "cli"
|
|
1456
1799
|
});
|
|
1457
1800
|
const authUrl = `${AUTH_BASE}/auth/desktop/start?${params.toString()}`;
|
|
1458
|
-
process.
|
|
1459
|
-
|
|
1801
|
+
const verbose = process.env.RSCOT_VERBOSE === "1";
|
|
1802
|
+
process.stdout.write("\nOpened your browser to sign in.\n");
|
|
1803
|
+
process.stdout.write("Complete the flow there, then return to this terminal.\n");
|
|
1804
|
+
process.stdout.write("Waiting (up to 3 minutes)\u2026\n\n");
|
|
1805
|
+
if (verbose) {
|
|
1806
|
+
process.stdout.write(`[verbose] auth url: ${authUrl}
|
|
1460
1807
|
`);
|
|
1461
|
-
|
|
1462
|
-
process.stdout.write(`Waiting for callback on http://127.0.0.1:${port}/cb (up to 3 minutes)\u2026
|
|
1808
|
+
process.stdout.write(`[verbose] loopback: 127.0.0.1:${port}
|
|
1463
1809
|
|
|
1464
1810
|
`);
|
|
1811
|
+
}
|
|
1465
1812
|
openBrowser(authUrl);
|
|
1466
1813
|
const code = await waitForCallback(server, state);
|
|
1467
1814
|
const bundle = await exchangeCode(code, verifier, deviceId);
|
|
@@ -1514,7 +1861,7 @@ async function refreshProfile() {
|
|
|
1514
1861
|
if (process.stdout.isTTY || process.env.FORCE_COLOR) {
|
|
1515
1862
|
kleur2.enabled = true;
|
|
1516
1863
|
}
|
|
1517
|
-
var
|
|
1864
|
+
var VERSION3 = "1.7.47";
|
|
1518
1865
|
var AUTH_FREE_COMMANDS = /* @__PURE__ */ new Set([
|
|
1519
1866
|
"login",
|
|
1520
1867
|
"logout",
|
|
@@ -1718,7 +2065,7 @@ async function dispatch(argv) {
|
|
|
1718
2065
|
async function main() {
|
|
1719
2066
|
const argv = process.argv.slice(2);
|
|
1720
2067
|
if (argv.includes("--version") || argv.includes("-v")) {
|
|
1721
|
-
process.stdout.write(
|
|
2068
|
+
process.stdout.write(VERSION3 + "\n");
|
|
1722
2069
|
return;
|
|
1723
2070
|
}
|
|
1724
2071
|
if (argv.includes("--help") || argv.includes("-h")) {
|