rscot-agent 1.7.48 → 1.7.49

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/index.js +2299 -1014
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -8,7 +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
+ import { existsSync as existsSync5, statSync as statSync2 } from "node:fs";
12
12
 
13
13
  // src/ui.ts
14
14
  import kleur from "kleur";
@@ -127,8 +127,8 @@ async function tryRun(cmd, args) {
127
127
  let resolvedPath;
128
128
  try {
129
129
  const which = process.platform === "win32" ? "where" : "which";
130
- const w2 = await pexec(which, [cmd], { timeout: 2e3, windowsHide: true });
131
- resolvedPath = w2.stdout.split(/\r?\n/)[0]?.trim();
130
+ const w3 = await pexec(which, [cmd], { timeout: 2e3, windowsHide: true });
131
+ resolvedPath = w3.stdout.split(/\r?\n/)[0]?.trim();
132
132
  } catch {
133
133
  }
134
134
  return { out: stdout.trim(), path: resolvedPath };
@@ -571,7 +571,7 @@ function loadConfig() {
571
571
  }
572
572
 
573
573
  // src/repl.ts
574
- import readline from "node:readline";
574
+ import readline2 from "node:readline";
575
575
  import { homedir as homedir4 } from "node:os";
576
576
 
577
577
  // src/agentRuntime.ts
@@ -694,1174 +694,2459 @@ function runAgent(opts) {
694
694
  });
695
695
  }
696
696
 
697
- // src/repl.ts
698
- var VERSION = "1.7.48";
699
- var BUILD = "a7f31c2";
700
- function homeShort(p) {
701
- const h = homedir4();
702
- return p.startsWith(h) ? "~" + p.slice(h.length) : p;
697
+ // src/nativeAgent.ts
698
+ import readline from "node:readline";
699
+
700
+ // ../tauri-shell/src/agent-core/permissions.ts
701
+ var DEFAULT_PERMISSIONS = {
702
+ writes: "ask",
703
+ // never auto by default — file writes are irreversible
704
+ commands: "ask",
705
+ // shell commands always confirm
706
+ deletes: "ask",
707
+ // deletes always confirm
708
+ network: "auto",
709
+ // network reads are low-risk
710
+ reads: "auto"
711
+ // pure reads always allowed
712
+ };
713
+ function categoryFor(toolName) {
714
+ if ([
715
+ "write_file",
716
+ "edit_file",
717
+ "apply_diff",
718
+ "apply_unified_diff",
719
+ "format_file"
720
+ ].includes(toolName))
721
+ return "writes";
722
+ if ([
723
+ "run_command",
724
+ "start_background_task",
725
+ "run_python_repl",
726
+ "run_lint"
727
+ ].includes(toolName))
728
+ return "commands";
729
+ if (["delete_file", "move_file", "rename_file"].includes(toolName))
730
+ return "deletes";
731
+ if ([
732
+ "web_search",
733
+ "verify_package",
734
+ "verify_package_batch",
735
+ "download_dataset",
736
+ "read_pdf"
737
+ ].includes(toolName))
738
+ return "network";
739
+ if ([
740
+ "read_file",
741
+ "list_dir",
742
+ "grep",
743
+ "glob",
744
+ "git_diff_summary",
745
+ "system_info",
746
+ "gpu_info"
747
+ ].includes(toolName))
748
+ return "reads";
749
+ return null;
703
750
  }
704
- function w(s) {
705
- process.stdout.write(s);
751
+
752
+ // ../tauri-shell/src/agent-core/loop.ts
753
+ async function runAgentLoop(driver) {
754
+ for (let iter = 0; iter < driver.maxTurns; iter++) {
755
+ let resp;
756
+ try {
757
+ resp = await driver.nextTurn();
758
+ } catch (err) {
759
+ driver.onTurnError(err);
760
+ return;
761
+ }
762
+ if (resp.type === "message") {
763
+ driver.onMessage(resp);
764
+ return;
765
+ }
766
+ if (resp.type === "done") {
767
+ await driver.onDone(resp);
768
+ return;
769
+ }
770
+ if (resp.type === "blocked") {
771
+ driver.onBlocked(resp);
772
+ continue;
773
+ }
774
+ driver.onToolCall(resp);
775
+ const cat = categoryFor(resp.name);
776
+ if (cat) {
777
+ const mode = driver.permissionMode(cat);
778
+ if (mode === "deny") {
779
+ driver.onRefused(resp, "policy");
780
+ continue;
781
+ }
782
+ if (mode === "ask") {
783
+ const ok = await driver.requestApproval(resp);
784
+ if (!ok) {
785
+ driver.onRefused(resp, "user");
786
+ continue;
787
+ }
788
+ }
789
+ }
790
+ const exec = await driver.executeTool(resp);
791
+ if (driver.isCancelled()) {
792
+ driver.onCancelledAfterTool(resp, exec);
793
+ return;
794
+ }
795
+ driver.onToolResult(resp, exec);
796
+ }
797
+ driver.onCapReached();
706
798
  }
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
- );
799
+
800
+ // ../tauri-shell/src/agent-core/apiClient.ts
801
+ function resolveGateway(cfg) {
802
+ if (cfg.provider === "custom") {
803
+ const dict = cfg.customHeaders || {};
804
+ const trimmedDict = {};
805
+ for (const [k, v] of Object.entries(dict)) {
806
+ const key = k.trim();
807
+ if (key)
808
+ trimmedDict[key] = v ?? "";
809
+ }
810
+ return {
811
+ base_url: (cfg.customBaseUrl || cfg.baseUrl || "").trim(),
812
+ model: (cfg.customModel || cfg.model || "").trim(),
813
+ auth_header: (cfg.customAuthHeader || "").trim() || void 0,
814
+ custom_headers: Object.keys(trimmedDict).length ? trimmedDict : void 0
815
+ };
816
+ }
817
+ return { base_url: cfg.baseUrl || "", model: cfg.model || "" };
715
818
  }
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");
819
+ var _rscotTokenCache = null;
820
+ var _onBearerRefreshed = null;
821
+ async function loadRscotToken() {
822
+ if (_rscotTokenCache !== null)
823
+ return _rscotTokenCache;
824
+ try {
825
+ const { invoke } = await import("@tauri-apps/api/core");
826
+ const tok = await invoke("read_rscot_token") || "";
827
+ _rscotTokenCache = tok;
828
+ return tok;
829
+ } catch {
830
+ _rscotTokenCache = "";
831
+ return "";
832
+ }
725
833
  }
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
834
+ var _refreshInFlight = null;
835
+ var _lastRefreshMs = 0;
836
+ async function refreshAndSyncBearer() {
837
+ if (_refreshInFlight)
838
+ return _refreshInFlight;
839
+ const sinceLast = Date.now() - _lastRefreshMs;
840
+ if (sinceLast < 3e3) {
841
+ try {
842
+ const { invoke } = await import("@tauri-apps/api/core");
843
+ const cached = (await invoke("auth_current_access_token") || "").trim();
844
+ if (cached)
845
+ return cached;
846
+ } catch {
847
+ }
848
+ }
849
+ _refreshInFlight = (async () => {
850
+ try {
851
+ const { invoke } = await import("@tauri-apps/api/core");
852
+ await invoke("auth_refresh_session");
853
+ const fresh = (await invoke("auth_current_access_token") || "").trim();
854
+ if (!fresh)
855
+ return null;
856
+ try {
857
+ _onBearerRefreshed?.(fresh);
858
+ } catch {
859
+ }
860
+ _lastRefreshMs = Date.now();
861
+ return fresh;
862
+ } catch {
863
+ return null;
864
+ } finally {
865
+ setTimeout(() => {
866
+ _refreshInFlight = null;
867
+ }, 0);
868
+ }
869
+ })();
870
+ return _refreshInFlight;
871
+ }
872
+ var ApiClient = class {
873
+ constructor(cfg) {
874
+ this.cfg = cfg;
875
+ }
876
+ currentBearer() {
877
+ return this.cfg.authBearer || "";
878
+ }
879
+ async authHeaders(overrideBearer) {
880
+ const bearer = overrideBearer ?? this.currentBearer();
881
+ const base = {
882
+ "Authorization": `Bearer ${bearer}`,
883
+ "X-CWA-Token": bearer,
884
+ "Content-Type": "application/json"
885
+ };
886
+ const tok = await loadRscotToken();
887
+ if (tok)
888
+ base["X-Rscot-Token"] = tok;
889
+ return base;
890
+ }
891
+ async doFetchWithRefresh(url, init) {
892
+ const res = await fetch(url, init);
893
+ if (res.status !== 401)
894
+ return res;
895
+ const fresh = await refreshAndSyncBearer();
896
+ if (!fresh)
897
+ return res;
898
+ this.cfg.authBearer = fresh;
899
+ const retryHeaders = {
900
+ ...init.headers,
901
+ "Authorization": `Bearer ${fresh}`,
902
+ "X-CWA-Token": fresh
903
+ };
904
+ return fetch(url, { ...init, headers: retryHeaders });
905
+ }
906
+ /* private sync headers() removed — unused; all callers use authHeaders() */
907
+ async turn(messages, opts) {
908
+ if (!messages.length)
909
+ throw new Error("turn() called with empty messages");
910
+ const body = {
911
+ messages,
912
+ provider: this.cfg.provider,
913
+ api_key: this.cfg.apiKey,
914
+ temperature: this.cfg.temperature
915
+ };
916
+ const gw = resolveGateway(this.cfg);
917
+ if (gw.model)
918
+ body.model = gw.model;
919
+ if (gw.base_url)
920
+ body.base_url = gw.base_url;
921
+ if (gw.auth_header)
922
+ body.auth_header = gw.auth_header;
923
+ if (gw.custom_headers)
924
+ body.custom_headers = gw.custom_headers;
925
+ if (this.cfg.stackHint)
926
+ body.stack_hint = this.cfg.stackHint;
927
+ if (this.cfg.architectModel)
928
+ body.architect_model = this.cfg.architectModel;
929
+ if (this.cfg.editorModel)
930
+ body.editor_model = this.cfg.editorModel;
931
+ if (this.cfg.reviewerModel)
932
+ body.reviewer_model = this.cfg.reviewerModel;
933
+ if (this.cfg.architectProvider)
934
+ body.architect_provider = this.cfg.architectProvider;
935
+ if (this.cfg.editorProvider)
936
+ body.editor_provider = this.cfg.editorProvider;
937
+ if (this.cfg.reviewerProvider)
938
+ body.reviewer_provider = this.cfg.reviewerProvider;
939
+ if (opts?.workspaceRoot)
940
+ body.workspace_root = opts.workspaceRoot;
941
+ if (opts?.sessionId)
942
+ body.session_id = opts.sessionId;
943
+ const res = await this.doFetchWithRefresh(`${this.cfg.backendUrl}/code-workspace/turn`, {
944
+ method: "POST",
945
+ headers: await this.authHeaders(),
946
+ body: JSON.stringify(body)
733
947
  });
734
- rl.setPrompt(promptStr);
735
- rl.prompt();
736
- let settled = false;
737
- const done = (v) => {
738
- if (settled)
739
- return;
740
- settled = true;
948
+ if (!res.ok) {
949
+ let detail = "";
741
950
  try {
742
- rl.close();
951
+ detail = (await res.json())?.detail || "";
743
952
  } catch {
953
+ detail = await res.text();
744
954
  }
745
- resolve2(v);
955
+ throw new Error(`backend ${res.status}: ${detail || res.statusText}`);
956
+ }
957
+ return await res.json();
958
+ }
959
+ /**
960
+ * Day 22 (π'-B) — SSE streaming sibling of turn(). Yields one StreamEvent
961
+ * at a time until a terminal event (done/message/tool_call/blocked/error).
962
+ * Identical wire protocol to the vscode-extension client; see
963
+ * backend/app/code_workspace_agent/streaming.py.
964
+ */
965
+ async *turnStream(messages, signal, opts) {
966
+ if (!messages.length)
967
+ throw new Error("turnStream() called with empty messages");
968
+ const body = {
969
+ messages,
970
+ provider: this.cfg.provider,
971
+ api_key: this.cfg.apiKey,
972
+ temperature: this.cfg.temperature
746
973
  };
747
- rl.once("line", (line) => done(line));
748
- rl.once("close", () => done(null));
749
- rl.on("SIGINT", () => done(null));
750
- });
974
+ const gw2 = resolveGateway(this.cfg);
975
+ if (gw2.model)
976
+ body.model = gw2.model;
977
+ if (gw2.base_url)
978
+ body.base_url = gw2.base_url;
979
+ if (gw2.auth_header)
980
+ body.auth_header = gw2.auth_header;
981
+ if (gw2.custom_headers)
982
+ body.custom_headers = gw2.custom_headers;
983
+ if (this.cfg.stackHint)
984
+ body.stack_hint = this.cfg.stackHint;
985
+ if (this.cfg.architectModel)
986
+ body.architect_model = this.cfg.architectModel;
987
+ if (this.cfg.editorModel)
988
+ body.editor_model = this.cfg.editorModel;
989
+ if (this.cfg.reviewerModel)
990
+ body.reviewer_model = this.cfg.reviewerModel;
991
+ if (this.cfg.architectProvider)
992
+ body.architect_provider = this.cfg.architectProvider;
993
+ if (this.cfg.editorProvider)
994
+ body.editor_provider = this.cfg.editorProvider;
995
+ if (this.cfg.reviewerProvider)
996
+ body.reviewer_provider = this.cfg.reviewerProvider;
997
+ if (opts?.workspaceRoot)
998
+ body.workspace_root = opts.workspaceRoot;
999
+ if (opts?.sessionId)
1000
+ body.session_id = opts.sessionId;
1001
+ const res = await this.doFetchWithRefresh(`${this.cfg.backendUrl}/code-workspace/turn-stream`, {
1002
+ method: "POST",
1003
+ headers: await this.authHeaders(),
1004
+ body: JSON.stringify(body),
1005
+ signal
1006
+ });
1007
+ if (!res.ok || !res.body) {
1008
+ let detail = "";
1009
+ try {
1010
+ detail = (await res.json())?.detail || "";
1011
+ } catch {
1012
+ detail = await res.text();
1013
+ }
1014
+ throw new Error(`backend ${res.status}: ${detail || res.statusText}`);
1015
+ }
1016
+ const reader = res.body.getReader();
1017
+ const decoder = new TextDecoder();
1018
+ let buf = "";
1019
+ while (true) {
1020
+ const { done, value } = await reader.read();
1021
+ if (done)
1022
+ break;
1023
+ buf += decoder.decode(value, { stream: true });
1024
+ let blockEnd;
1025
+ while ((blockEnd = buf.indexOf("\n\n")) !== -1) {
1026
+ const block = buf.slice(0, blockEnd);
1027
+ buf = buf.slice(blockEnd + 2);
1028
+ const ev = parseSseBlock(block);
1029
+ if (ev)
1030
+ yield ev;
1031
+ }
1032
+ }
1033
+ }
1034
+ async stacks() {
1035
+ const r = await this.doFetchWithRefresh(`${this.cfg.backendUrl}/code-workspace/stacks`, {
1036
+ headers: await this.authHeaders()
1037
+ });
1038
+ if (!r.ok)
1039
+ return { stacks: [] };
1040
+ return r.json();
1041
+ }
1042
+ async getJson(pathAndQuery) {
1043
+ const r = await this.doFetchWithRefresh(`${this.cfg.backendUrl}${pathAndQuery}`, {
1044
+ headers: await this.authHeaders()
1045
+ });
1046
+ if (!r.ok)
1047
+ return { error: `backend ${r.status}` };
1048
+ return r.json();
1049
+ }
1050
+ async postJson(pathAndQuery, body) {
1051
+ const r = await this.doFetchWithRefresh(`${this.cfg.backendUrl}${pathAndQuery}`, {
1052
+ method: "POST",
1053
+ headers: await this.authHeaders(),
1054
+ body: JSON.stringify(body)
1055
+ });
1056
+ if (!r.ok)
1057
+ return { error: `backend ${r.status}` };
1058
+ return r.json();
1059
+ }
1060
+ };
1061
+ function parseSseBlock(block) {
1062
+ let event = "message";
1063
+ let data = "";
1064
+ for (const line of block.split("\n")) {
1065
+ if (line.startsWith("event:"))
1066
+ event = line.slice(6).trim();
1067
+ else if (line.startsWith("data:"))
1068
+ data = (data ? data + "\n" : "") + line.slice(5).trim();
1069
+ }
1070
+ if (!data)
1071
+ return null;
1072
+ try {
1073
+ const parsed = JSON.parse(data);
1074
+ return { type: event, ...parsed };
1075
+ } catch {
1076
+ return null;
1077
+ }
751
1078
  }
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;
1079
+
1080
+ // ../tauri-shell/src/agent-core/host.ts
1081
+ var HostUnavailableError = class extends Error {
1082
+ constructor(capability) {
1083
+ super(`Host capability '${capability}' is not available in the current runtime. Launch with 'cargo tauri dev' for native filesystem + shell access.`);
757
1084
  }
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;
1085
+ };
1086
+
1087
+ // ../tauri-shell/src/agent-core/executors.ts
1088
+ async function execute(name, args, ctx) {
1089
+ try {
1090
+ switch (name) {
1091
+ case "list_dir":
1092
+ return await listDir(args, ctx);
1093
+ case "read_file":
1094
+ return await readFile(args, ctx);
1095
+ case "format_file":
1096
+ return await formatFile(args, ctx);
1097
+ case "write_file":
1098
+ return await writeFile(args, ctx);
1099
+ case "edit_file":
1100
+ return await editFile(args, ctx);
1101
+ case "glob":
1102
+ return await glob(args, ctx);
1103
+ case "grep":
1104
+ return await grep(args, ctx);
1105
+ case "run_command":
1106
+ return await runCommand(args, ctx);
1107
+ case "system_info":
1108
+ if (!ctx.host.capabilities.filesystem)
1109
+ return notImpl("system_info");
1110
+ return await ctx.host.systemInfo(args?.level === "full" ? "full" : "brief");
1111
+ case "allocate_port":
1112
+ if (!ctx.host.capabilities.filesystem)
1113
+ return notImpl("allocate_port");
1114
+ return await ctx.host.allocatePort({ prefer: args?.prefer, range: args?.range });
1115
+ case "list_processes":
1116
+ if (!ctx.host.capabilities.filesystem)
1117
+ return notImpl("list_processes");
1118
+ return await ctx.host.listProcesses(args?.include_finished === true);
1119
+ case "kill_process":
1120
+ if (!ctx.host.capabilities.filesystem)
1121
+ return notImpl("kill_process");
1122
+ return await ctx.host.killProcess(args.pid, args.signal ?? "SIGTERM");
1123
+ case "verify_package":
1124
+ return ctx.api.getJson(
1125
+ `/code-workspace/verify-package?ecosystem=${encodeURIComponent(args.ecosystem)}&name=${encodeURIComponent(args.name)}`
1126
+ );
1127
+ case "verify_package_batch":
1128
+ return ctx.api.postJson(
1129
+ `/code-workspace/verify-package-batch`,
1130
+ { packages: args.packages || [] }
1131
+ );
1132
+ case "web_search": {
1133
+ const p = new URLSearchParams({ query: args.query });
1134
+ if (args.sources?.length)
1135
+ p.set("sources", args.sources.join(","));
1136
+ if (args.max_per_source)
1137
+ p.set("max_per_source", String(args.max_per_source));
1138
+ return ctx.api.getJson(`/code-workspace/web-search?${p.toString()}`);
1139
+ }
1140
+ case "gpu_info":
1141
+ return ctx.api.getJson(`/code-workspace/gpu-info`);
1142
+ case "start_background_task": {
1143
+ const cwd = args.cwd && args.cwd.trim() || (ctx.host.workspaceRoot() || "");
1144
+ try {
1145
+ const { invoke } = await import("@tauri-apps/api/core");
1146
+ return await invoke("local_bg_start", {
1147
+ command: args.command,
1148
+ cwd,
1149
+ label: args.label || ""
1150
+ });
1151
+ } catch (e) {
1152
+ return { error: `local_bg_start failed: ${e?.message || String(e)}` };
1153
+ }
1154
+ }
1155
+ case "check_background_task": {
1156
+ try {
1157
+ const { invoke } = await import("@tauri-apps/api/core");
1158
+ return await invoke("local_bg_check", {
1159
+ taskId: args.task_id,
1160
+ tailLines: args.tail_lines || 200
1161
+ });
1162
+ } catch (e) {
1163
+ return { error: `local_bg_check failed: ${e?.message || String(e)}` };
1164
+ }
1165
+ }
1166
+ case "list_background_tasks": {
1167
+ try {
1168
+ const { invoke } = await import("@tauri-apps/api/core");
1169
+ const tasks = await invoke("local_bg_list");
1170
+ return { tasks };
1171
+ } catch (e) {
1172
+ return { error: `local_bg_list failed: ${e?.message || String(e)}` };
1173
+ }
1174
+ }
1175
+ case "cancel_background_task": {
1176
+ try {
1177
+ const { invoke } = await import("@tauri-apps/api/core");
1178
+ return await invoke("local_bg_cancel", {
1179
+ taskId: args.task_id,
1180
+ signal: args.signal || "TERM"
1181
+ });
1182
+ } catch (e) {
1183
+ return { error: `local_bg_cancel failed: ${e?.message || String(e)}` };
1184
+ }
1185
+ }
1186
+ case "mount_external_folder":
1187
+ return await mountExternalFolder(args, ctx);
1188
+ case "download_dataset":
1189
+ return await downloadDataset(args, ctx);
1190
+ case "run_lint":
1191
+ return notImpl("run_lint");
1192
+ case "create_pr":
1193
+ return notImpl("create_pr");
1194
+ case "agent_self_eval": {
1195
+ const files = args.files || [];
1196
+ const fileContents = {};
1197
+ if (ctx.host.capabilities.filesystem && ctx.host.workspaceRoot()) {
1198
+ for (const rel of files.slice(0, 20)) {
1199
+ try {
1200
+ const abs = joinPath(ctx.host.workspaceRoot(), rel);
1201
+ fileContents[rel] = (await ctx.host.readFile(abs)).slice(0, 4e4);
1202
+ } catch (e) {
1203
+ fileContents[rel] = `[error reading: ${e?.message || e}]`;
1204
+ }
1205
+ }
1206
+ }
1207
+ return ctx.api.postJson("/code-workspace/self-eval", {
1208
+ stack_hint: args.stack_hint,
1209
+ files,
1210
+ summary: args.summary,
1211
+ file_contents: fileContents
1212
+ });
1213
+ }
1214
+ default:
1215
+ return { error: "unknown_tool", name };
1216
+ }
1217
+ } catch (err) {
1218
+ if (err instanceof HostUnavailableError) {
1219
+ return {
1220
+ error: "host_unavailable",
1221
+ capability: err.message,
1222
+ hint: "This tool needs the native Tauri shell. The chat panel works in browser preview but filesystem + shell tools don't."
1223
+ };
1224
+ }
1225
+ return { error: err?.name || "ExecutorError", message: err?.message || String(err), tool: name };
764
1226
  }
765
- return runAgent({ workspace, backendUrl: backendUrl2, prompt });
766
1227
  }
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;
1228
+ var notImpl = (n) => ({
1229
+ error: "host_unavailable",
1230
+ tool: n,
1231
+ hint: "Launch with `cargo tauri dev` for native host capabilities. In web preview, this tool is stubbed."
1232
+ });
1233
+ async function listDir(args, ctx) {
1234
+ const root = ctx.host.workspaceRoot();
1235
+ if (!root)
1236
+ return { error: "no_workspace", hint: "Pick a folder first." };
1237
+ const abs = joinPath(root, args.path || ".");
1238
+ const entries = await ctx.host.readDir(abs, true, 500);
1239
+ return { path: args.path || ".", entries, truncated: entries.length >= 500 };
1240
+ }
1241
+ async function readFile(args, ctx) {
1242
+ const root = ctx.host.workspaceRoot();
1243
+ if (!root)
1244
+ return { error: "no_workspace" };
1245
+ const abs = joinPath(root, args.path);
1246
+ const stat = await ctx.host.stat(abs);
1247
+ if (!stat || !stat.isFile)
1248
+ return { error: "not_a_file", path: args.path };
1249
+ if (stat.size > 200 * 1024 && (args.start_line == null || args.end_line == null)) {
1250
+ return { error: "too_large", size_bytes: stat.size, hint: "specify start_line + end_line" };
1251
+ }
1252
+ const content = await ctx.host.readFile(abs);
1253
+ const lines = content.split(/\r?\n/);
1254
+ if (args.start_line || args.end_line) {
1255
+ const s = Math.max(1, args.start_line || 1);
1256
+ const e = Math.min(lines.length, args.end_line || lines.length);
1257
+ return {
1258
+ path: args.path,
1259
+ content: lines.slice(s - 1, e).join("\n"),
1260
+ encoding: "utf-8",
1261
+ total_lines: lines.length,
1262
+ returned_lines: [s, e]
1263
+ };
772
1264
  }
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");
1265
+ return {
1266
+ path: args.path,
1267
+ content,
1268
+ encoding: "utf-8",
1269
+ total_lines: lines.length,
1270
+ returned_lines: [1, lines.length]
1271
+ };
1272
+ }
1273
+ async function writeFile(args, ctx) {
1274
+ const root = ctx.host.workspaceRoot();
1275
+ if (!root)
1276
+ return { error: "no_workspace" };
1277
+ const abs = joinPath(root, args.path);
1278
+ let previous = null;
1279
+ try {
1280
+ previous = await ctx.host.readFile(abs);
1281
+ } catch {
782
1282
  }
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;
1283
+ ctx.rollback.set(abs, previous);
1284
+ const parent = abs.replace(/[/\\][^/\\]+$/, "");
1285
+ if (parent !== abs) {
1286
+ try {
1287
+ await ctx.host.mkdir(parent, true);
1288
+ } catch {
799
1289
  }
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"));
1290
+ }
1291
+ const r = await ctx.host.writeFile(abs, args.content);
1292
+ return {
1293
+ path: args.path,
1294
+ ...r,
1295
+ rollback_token: `rb_${Date.now().toString(36)}_${ctx.rollback.size}`
1296
+ };
1297
+ }
1298
+ async function editFile(args, ctx) {
1299
+ const root = ctx.host.workspaceRoot();
1300
+ if (!root)
1301
+ return { error: "no_workspace" };
1302
+ const expected = args.expect_count ?? 1;
1303
+ const abs = joinPath(root, args.path);
1304
+ const content = await ctx.host.readFile(abs);
1305
+ let count = 0, idx = 0;
1306
+ while ((idx = content.indexOf(args.search, idx)) !== -1) {
1307
+ count++;
1308
+ idx += args.search.length;
1309
+ }
1310
+ if (count !== expected) {
1311
+ return {
1312
+ error: "match_count_mismatch",
1313
+ expected_count: expected,
1314
+ actual_count: count,
1315
+ hint: count === 0 ? "search string not found \u2014 re-read the file" : `widen 'search' or pass expect_count=${count}`
1316
+ };
1317
+ }
1318
+ ctx.rollback.set(abs, content);
1319
+ const next = content.split(args.search).join(args.replace);
1320
+ await ctx.host.writeFile(abs, next);
1321
+ return { path: args.path, applied: true, matches_replaced: count };
1322
+ }
1323
+ async function glob(args, ctx) {
1324
+ const root = ctx.host.workspaceRoot();
1325
+ if (!root)
1326
+ return { error: "no_workspace" };
1327
+ const abs = joinPath(root, args.path || ".");
1328
+ const max = Math.min(args.max_results ?? 200, 2e3);
1329
+ const re = globToRegExp(args.pattern);
1330
+ const all = await ctx.host.readDir(abs, true, max * 5);
1331
+ const matches = all.filter((e) => !e.is_dir && re.test(e.path)).slice(0, max).map((e) => e.path);
1332
+ return { pattern: args.pattern, matches, truncated: matches.length >= max };
1333
+ }
1334
+ async function grep(args, ctx) {
1335
+ const root = ctx.host.workspaceRoot();
1336
+ if (!root)
1337
+ return { error: "no_workspace" };
1338
+ const isRegex = args.regex !== false;
1339
+ const re = isRegex ? new RegExp(args.pattern) : new RegExp(args.pattern.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"));
1340
+ const fileRe = globToRegExp(args.file_glob || "**/*");
1341
+ const max = Math.min(args.max_matches ?? 200, 2e3);
1342
+ const abs = joinPath(root, args.path || ".");
1343
+ const entries = await ctx.host.readDir(abs, true, 2e3);
1344
+ const hits = [];
1345
+ for (const e of entries) {
1346
+ if (e.is_dir || !fileRe.test(e.path))
1347
+ continue;
1348
+ if (e.size_bytes != null && e.size_bytes > 1e6)
1349
+ continue;
1350
+ let txt;
1351
+ try {
1352
+ txt = await ctx.host.readFile(joinPath(root, e.path));
1353
+ } catch {
804
1354
  continue;
805
1355
  }
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"));
1356
+ const lines = txt.split(/\r?\n/);
1357
+ for (let i = 0; i < lines.length; i++) {
1358
+ if (re.test(lines[i])) {
1359
+ hits.push({ path: e.path, line: i + 1, content: lines[i].slice(0, 400) });
1360
+ if (hits.length >= max)
1361
+ break;
814
1362
  }
815
- continue;
816
1363
  }
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");
1364
+ if (hits.length >= max)
1365
+ break;
1366
+ }
1367
+ return { pattern: args.pattern, hits, truncated: hits.length >= max };
1368
+ }
1369
+ async function runCommand(args, ctx) {
1370
+ const root = ctx.host.workspaceRoot();
1371
+ if (!root)
1372
+ return { error: "no_workspace" };
1373
+ const timeoutMs = Math.min(Math.max(args.timeout_s ?? 60, 1), 120) * 1e3;
1374
+ const cwd = args.cwd ? joinPath(root, args.cwd) : root;
1375
+ const r = await ctx.host.runCommand(args.command, { cwd, timeoutMs });
1376
+ return { command: args.command, ...r, spawned_pid: null };
1377
+ }
1378
+ async function formatFile(args, ctx) {
1379
+ const root = ctx.host.workspaceRoot();
1380
+ if (!root)
1381
+ return { error: "no_workspace" };
1382
+ const abs = joinPath(root, args.path);
1383
+ const checkFlag = args.check_only === true ? " --check" : "";
1384
+ const r = await ctx.host.runCommand(`ruff format${checkFlag} "${abs}"`, {
1385
+ cwd: root,
1386
+ timeoutMs: 3e4
1387
+ });
1388
+ return {
1389
+ tool_used: "ruff",
1390
+ applied: r.exit_code === 0 && !args.check_only,
1391
+ ...r
1392
+ };
1393
+ }
1394
+ var _PROTECTED_PREFIXES = [
1395
+ "/etc",
1396
+ "/usr/bin",
1397
+ "/usr/sbin",
1398
+ "/bin",
1399
+ "/sbin",
1400
+ "/sys",
1401
+ "/proc",
1402
+ "/dev",
1403
+ "c:\\windows",
1404
+ "c:\\program files",
1405
+ "c:\\program files (x86)",
1406
+ "c:\\programdata"
1407
+ ];
1408
+ function _isProtected(abs) {
1409
+ const n = abs.replace(/\\/g, "/").toLowerCase();
1410
+ for (const p of _PROTECTED_PREFIXES) {
1411
+ const np = p.replace(/\\/g, "/").toLowerCase();
1412
+ if (n === np || n.startsWith(np + "/"))
1413
+ return p;
1414
+ }
1415
+ return null;
1416
+ }
1417
+ async function mountExternalFolder(args, ctx) {
1418
+ const raw = String(args?.path || "").trim();
1419
+ if (!raw)
1420
+ return { ok: false, error: "empty path" };
1421
+ const purpose = String(args?.purpose || "").trim();
1422
+ const abs = raw.replace(/\\/g, "/").replace(/\/+$/, "");
1423
+ const proto = _isProtected(abs);
1424
+ if (proto) {
1425
+ return { ok: false, error: "protected_host_dir", refused: proto };
1426
+ }
1427
+ if (!ctx.host.capabilities.filesystem) {
1428
+ return {
1429
+ ok: false,
1430
+ error: "filesystem_unavailable",
1431
+ hint: "this surface (web preview) cannot read external folders"
1432
+ };
1433
+ }
1434
+ const st = await ctx.host.stat(abs);
1435
+ if (!st || !st.isDirectory) {
1436
+ return { ok: false, error: "not_a_directory", path: abs };
1437
+ }
1438
+ if (ctx.mountedFolders.has(abs)) {
1439
+ return { ok: true, already_mounted: true, path: abs, mode: "read-only" };
1440
+ }
1441
+ const ok = typeof window !== "undefined" && typeof window.confirm === "function" ? window.confirm(
1442
+ `Allow the agent READ-ONLY access to:
1443
+ ${abs}
1444
+
1445
+ Purpose: ${purpose || "(none given)"}`
1446
+ ) : true;
1447
+ if (!ok)
1448
+ return { ok: false, error: "user_denied", path: abs };
1449
+ ctx.mountedFolders.add(abs);
1450
+ return {
1451
+ ok: true,
1452
+ mounted: abs,
1453
+ mode: "read-only",
1454
+ purpose,
1455
+ total_mounted: ctx.mountedFolders.size
1456
+ };
1457
+ }
1458
+ function _sanitize(s) {
1459
+ return s.replace(/[/\\:?"<>|*]/g, "_").replace(/_+/g, "_").slice(0, 80);
1460
+ }
1461
+ async function downloadDataset(args, ctx) {
1462
+ const source = String(args?.source || "");
1463
+ const name = String(args?.name_or_url || "").trim();
1464
+ if (!name)
1465
+ return { ok: false, error: "name_or_url is empty" };
1466
+ if (!["huggingface", "kaggle", "github", "url"].includes(source)) {
1467
+ return { ok: false, error: `unknown source ${JSON.stringify(source)}` };
1468
+ }
1469
+ const root = ctx.host.workspaceRoot();
1470
+ if (!root)
1471
+ return { ok: false, error: "no workspace folder set" };
1472
+ const dest = String(args?.dest_path || `.datasets/${source}-${_sanitize(name)}`);
1473
+ const destAbs = joinPath(root, dest);
1474
+ let command;
1475
+ switch (source) {
1476
+ case "huggingface": {
1477
+ const tier = args?.kind === "model" ? "" : "--repo-type dataset ";
1478
+ command = `huggingface-cli download ${tier}${name} --local-dir "${destAbs}" --local-dir-use-symlinks False`;
1479
+ break;
1480
+ }
1481
+ case "kaggle":
1482
+ command = `kaggle datasets download -d ${name} -p "${destAbs}" --unzip`;
1483
+ break;
1484
+ case "github": {
1485
+ const url = name.startsWith("http") ? name : `https://github.com/${name}.git`;
1486
+ command = `git clone --depth=1 ${url} "${destAbs}"`;
1487
+ break;
1488
+ }
1489
+ case "url": {
1490
+ const filename = name.split("/").pop() || "download";
1491
+ const safe = _sanitize(filename);
1492
+ command = `curl -L -o "${joinPath(destAbs, safe)}" "${name}"`;
1493
+ break;
1494
+ }
1495
+ default:
1496
+ return { ok: false, error: `unsupported source ${source}` };
1497
+ }
1498
+ const taskRes = await ctx.api.postJson(`/code-workspace/bg-task/start`, {
1499
+ command,
1500
+ cwd: root,
1501
+ label: `download:${source}:${_sanitize(name).slice(0, 40)}`,
1502
+ env_extra: null
1503
+ });
1504
+ return {
1505
+ ok: true,
1506
+ source,
1507
+ name,
1508
+ dest_path: dest,
1509
+ dest_abs: destAbs,
1510
+ command_running: command,
1511
+ task_id: taskRes?.task_id || null,
1512
+ note: "running as background task \u2014 use check_background_task to poll",
1513
+ task_info: taskRes
1514
+ };
1515
+ }
1516
+ function joinPath(root, rel) {
1517
+ if (!rel || rel === ".")
1518
+ return root;
1519
+ const sep = root.includes("\\") && !root.includes("/") ? "\\" : "/";
1520
+ const cleaned = rel.replace(/^[\\/]+/, "");
1521
+ return root.replace(/[/\\]+$/, "") + sep + cleaned.replace(/[\\/]+/g, sep);
1522
+ }
1523
+ function globToRegExp(pattern) {
1524
+ let re = "^";
1525
+ let i = 0;
1526
+ while (i < pattern.length) {
1527
+ const c = pattern[i];
1528
+ if (c === "*") {
1529
+ if (pattern[i + 1] === "*") {
1530
+ if (pattern[i + 2] === "/") {
1531
+ re += "(?:.*/)?";
1532
+ i += 3;
1533
+ } else {
1534
+ re += ".*";
1535
+ i += 2;
1536
+ }
1537
+ } else {
1538
+ re += "[^/]*";
1539
+ i++;
1540
+ }
1541
+ } else if (c === "?") {
1542
+ re += "[^/]";
1543
+ i++;
1544
+ } else if (c === ".") {
1545
+ re += "\\.";
1546
+ i++;
1547
+ } else if (c === "+") {
1548
+ re += "\\+";
1549
+ i++;
1550
+ } else if (c === "(" || c === ")" || c === "|" || c === "$" || c === "^") {
1551
+ re += "\\" + c;
1552
+ i++;
1553
+ } else {
1554
+ re += c;
1555
+ i++;
827
1556
  }
828
- w("\n");
829
1557
  }
830
- w("\n" + palette.dim(" bye \xB7 ") + palette.dim(turnCount + " turn" + (turnCount === 1 ? "" : "s") + " this session\n\n"));
831
- return 0;
1558
+ re += "$";
1559
+ return new RegExp(re);
832
1560
  }
833
1561
 
834
- // src/install.ts
835
- var VERSION2 = "1.7.48";
836
- var BUILD2 = "a7f31c2";
837
- function backendUrl() {
838
- return process.env.RSCOT_BACKEND_URL || "http://localhost:8010";
1562
+ // ../tauri-shell/src/agent-core/host-node.ts
1563
+ async function nodeFs() {
1564
+ return import("node:fs/promises");
839
1565
  }
840
- function nl(n = 1) {
841
- process.stdout.write("\n".repeat(n));
1566
+ async function nodeCp() {
1567
+ return import("node:child_process");
842
1568
  }
843
- function println(s = "") {
844
- process.stdout.write(s + "\n");
1569
+ async function nodeOs() {
1570
+ return import("node:os");
845
1571
  }
846
- function sleep(ms) {
847
- return new Promise((r) => setTimeout(r, ms));
1572
+ async function nodeNet() {
1573
+ return import("node:net");
848
1574
  }
849
- async function runEnvironmentProbe() {
850
- println(palette.prompt("$") + " " + palette.cmd("npx rscot"));
851
- nl();
852
- println(brandHeader(VERSION2, BUILD2));
853
- nl();
854
- println(sectionTitle("Checking your environment\u2026"));
855
- nl();
856
- const labels = ["node", "npm registry", "python", "git", "llm provider"];
857
- for (const label of labels) {
858
- const filled = label === "node" || label === "npm registry" ? 100 : label === "python" ? 83 : label === "git" ? 66 : 33;
859
- const bar = progressBar(filled, 18);
860
- const right = filled === 100 ? palette.success("OK") : palette.dim("scanning $PATH");
861
- println(
862
- " " + palette.cmd(label.padEnd(14, " ")) + " " + bar + " " + ("" + filled + "%").padStart(5, " ") + " " + right
863
- );
864
- await sleep(120);
865
- }
866
- nl();
867
- const results = await runAllChecks();
868
- return results;
869
- }
870
- function printEnvironmentReport(results) {
871
- println(sectionTitle("Environment report"));
872
- println(rule());
873
- for (const r of results) {
874
- const status = r.ok ? "ok" : r.optional ? "warn" : "fail";
875
- println(taskRow(r.name, status, r.detail));
1575
+ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "__pycache__", ".venv", "target", "dist", "build"]);
1576
+ var NodeHost = class {
1577
+ capabilities = { filesystem: true, shell: true };
1578
+ root = null;
1579
+ workspaceRoot() {
1580
+ return this.root;
876
1581
  }
877
- println(rule());
878
- nl();
879
- const hasKey = results.find((r) => r.name === "LLM provider key")?.ok;
880
- if (!hasKey) {
881
- println(
882
- " " + palette.warn("\u25B2") + " Rscot needs " + palette.bold("one") + " LLM provider key to operate."
883
- );
884
- println(
885
- " We'll walk you through it. Keys are stored under " + palette.cyan("~/.rscot/keys/") + " with chmod 600,"
886
- );
887
- println(
888
- " never written to disk in plain text outside that dir, never transmitted to the Rscot project."
889
- );
890
- nl();
1582
+ setWorkspaceRoot(p) {
1583
+ this.root = p;
891
1584
  }
892
- }
893
- async function pickProvider() {
894
- const choices = providers.map((p2) => {
895
- const badge = p2.recommended ? palette.accent(" (recommended)") : "";
896
- return {
897
- title: palette.bold(p2.display) + badge,
898
- description: p2.blurb,
899
- value: p2.id
900
- };
901
- });
902
- const { provider } = await prompts(
903
- {
904
- type: "select",
905
- name: "provider",
906
- message: "Which LLM provider would you like to start with?",
907
- choices,
908
- initial: 0,
909
- hint: "\u2191/\u2193 to move \xB7 enter to select \xB7 esc to cancel"
910
- },
911
- {
912
- onCancel: () => {
913
- return false;
914
- }
915
- }
916
- );
917
- if (!provider)
918
- return null;
919
- const p = findProvider(provider);
920
- if (!p)
921
- return null;
922
- if (p.id === "skip") {
923
- return p;
1585
+ async readFile(absPath) {
1586
+ const fs = await nodeFs();
1587
+ return await fs.readFile(absPath, "utf8");
924
1588
  }
925
- println(rule());
926
- println(
927
- " " + palette.accent(p.display) + " " + palette.dim("\u2192") + " default " + palette.cyan(p.defaultModel)
928
- );
929
- if (p.models.length > 1) {
930
- for (const m of p.models.slice(1)) {
931
- println(
932
- " " + " ".repeat(p.display.length) + " " + palette.dim("\u2192") + " alt model " + palette.cyan(m.label) + (m.note ? " " + palette.dim("(" + m.note + ")") : "")
933
- );
1589
+ async writeFile(absPath, content) {
1590
+ const fs = await nodeFs();
1591
+ let previous_size = null;
1592
+ let created = true;
1593
+ try {
1594
+ const st = await fs.stat(absPath);
1595
+ previous_size = Number(st.size);
1596
+ created = false;
1597
+ } catch {
934
1598
  }
1599
+ await fs.writeFile(absPath, content, "utf8");
1600
+ return { bytes_written: new TextEncoder().encode(content).length, created, previous_size };
935
1601
  }
936
- if (p.keyHelpUrl) {
937
- println(
938
- " " + " ".repeat(p.display.length) + " " + palette.dim("\u2192") + " signup " + palette.cyan(p.keyHelpUrl)
939
- );
1602
+ async unlink(absPath) {
1603
+ const fs = await nodeFs();
1604
+ await fs.rm(absPath, { force: true });
940
1605
  }
941
- nl();
942
- return p;
943
- }
944
- async function pasteKey(p) {
945
- println(palette.success("\u2713") + " Provider: " + palette.bold(p.display));
946
- println(" " + palette.dim("\u203A model ") + " " + palette.cyan(p.defaultModel));
947
- if (p.keyHelpUrl) {
948
- println(" " + palette.dim("\u203A signup ") + " " + palette.cyan(p.keyHelpUrl));
1606
+ async mkdir(absPath, recursive) {
1607
+ const fs = await nodeFs();
1608
+ await fs.mkdir(absPath, { recursive });
949
1609
  }
950
- nl();
951
- if (!p.needsKey) {
952
- println(palette.dim(" (no API key needed for " + p.display + " \u2014 local bridge)"));
953
- nl();
954
- return "";
1610
+ async stat(absPath) {
1611
+ const fs = await nodeFs();
1612
+ try {
1613
+ const s = await fs.stat(absPath);
1614
+ return { size: Number(s.size), isFile: s.isFile(), isDirectory: s.isDirectory() };
1615
+ } catch {
1616
+ return null;
1617
+ }
955
1618
  }
956
- const { key } = await prompts(
957
- {
958
- type: "password",
959
- name: "key",
960
- message: "Paste your " + p.display.charAt(0).toUpperCase() + p.display.slice(1) + " API key (masked \xB7 stored under ~/.rscot/keys with chmod 600 \xB7 ctrl-c aborts)",
961
- validate: (v) => {
962
- if (!v || v.trim().length < p.keyMinLen) {
963
- return "key too short (expected >= " + p.keyMinLen + " chars)";
964
- }
965
- if (p.keyPrefix && !v.startsWith(p.keyPrefix)) {
966
- return "expected prefix '" + p.keyPrefix + "' \u2014 looks like the wrong key";
1619
+ async readDir(absPath, recursive, maxEntries) {
1620
+ const fs = await nodeFs();
1621
+ const out = [];
1622
+ const root = this.root || absPath;
1623
+ const sep = process.platform === "win32" ? "\\" : "/";
1624
+ const walk = async (dir) => {
1625
+ if (out.length >= maxEntries)
1626
+ return;
1627
+ let items;
1628
+ try {
1629
+ items = await fs.readdir(dir, { withFileTypes: true });
1630
+ } catch {
1631
+ return;
1632
+ }
1633
+ for (const it of items) {
1634
+ if (out.length >= maxEntries)
1635
+ return;
1636
+ if (it.isDirectory() && SKIP_DIRS.has(it.name))
1637
+ continue;
1638
+ const full = dir.endsWith(sep) ? dir + it.name : dir + sep + it.name;
1639
+ const rel = relativize(root, full);
1640
+ if (it.isFile()) {
1641
+ let size = null;
1642
+ try {
1643
+ size = Number((await fs.stat(full)).size);
1644
+ } catch {
1645
+ }
1646
+ out.push({ path: rel, size_bytes: size, is_dir: false });
1647
+ } else if (it.isDirectory()) {
1648
+ out.push({ path: rel, size_bytes: null, is_dir: true });
1649
+ if (recursive)
1650
+ await walk(full);
967
1651
  }
968
- return true;
969
1652
  }
970
- },
971
- {
972
- onCancel: () => false
973
- }
974
- );
975
- if (!key)
976
- return null;
977
- println(
978
- " " + palette.dim("\u21B3 characters: ") + palette.cyan("" + key.length) + " " + palette.dim("\u21B3 format: ") + palette.success("looks valid" + (p.keyPrefix ? " (" + p.keyPrefix + "\u2026)" : ""))
979
- );
980
- nl();
981
- return key.trim();
982
- }
983
- async function runInstallPhases(provider, apiKey) {
984
- const cfgDir = ensureConfigDir();
985
- if (provider.needsKey) {
986
- println(
987
- palette.success("\u2713") + " Key staged for " + palette.bold(provider.display) + " " + palette.dim("service=rscot account=" + provider.id)
988
- );
989
- println(
990
- palette.success("\u2713") + " Validating key format " + palette.dim(apiKey.length + " chars \xB7 prefix ok")
991
- );
992
- } else {
993
- println(
994
- palette.success("\u2713") + " Provider needs no key " + palette.dim("ollama bridge will be probed on first run")
995
- );
1653
+ };
1654
+ await walk(absPath);
1655
+ return out;
1656
+ }
1657
+ async runCommand(cmd, opts) {
1658
+ const cp = await nodeCp();
1659
+ const isWin = process.platform === "win32";
1660
+ const started = Date.now();
1661
+ return await new Promise((resolve2) => {
1662
+ let stdout = "", stderr = "", timed_out = false, settled = false;
1663
+ const child = isWin ? cp.spawn("cmd", ["/d", "/s", "/c", cmd], { cwd: opts.cwd, windowsHide: true }) : cp.spawn("sh", ["-c", cmd], { cwd: opts.cwd });
1664
+ const timer = setTimeout(() => {
1665
+ timed_out = true;
1666
+ try {
1667
+ child.kill("SIGKILL");
1668
+ } catch {
1669
+ }
1670
+ }, Math.max(1, opts.timeoutMs));
1671
+ child.stdout?.on("data", (d) => {
1672
+ stdout += d.toString();
1673
+ });
1674
+ child.stderr?.on("data", (d) => {
1675
+ stderr += d.toString();
1676
+ });
1677
+ const done = (exit_code) => {
1678
+ if (settled)
1679
+ return;
1680
+ settled = true;
1681
+ clearTimeout(timer);
1682
+ resolve2({ exit_code, stdout, stderr, duration_ms: Date.now() - started, timed_out });
1683
+ };
1684
+ child.on("error", (err) => {
1685
+ stderr += String(err?.message || err);
1686
+ done(-1);
1687
+ });
1688
+ child.on("close", (code) => done(timed_out ? -1 : code ?? 0));
1689
+ });
996
1690
  }
997
- nl();
998
- println(sectionTitle("Installing Rscot Agent v" + VERSION2));
999
- println(rule());
1000
- nl();
1001
- const phases = [
1002
- { label: "resolving deps", detail: "47 packages \xB7 lockfile pinned", ms: 450 },
1003
- { label: "fetching wheels", detail: "fastapi \xB7 uvicorn \xB7 httpx \xB7 ripgrep", ms: 620 },
1004
- { label: "building backend", detail: "compiling rust core (ripgrep bindings)", ms: 780 },
1005
- { label: "linking CLI", detail: "shim \u2192 " + cfgDir.path, ms: 380 },
1006
- { label: "writing config", detail: "saving provider + key reference", ms: 320 }
1007
- ];
1008
- const total = phases.length;
1009
- const startedAt = Date.now();
1010
- for (let i = 0; i < phases.length; i++) {
1011
- const phase = phases[i];
1012
- const tag = phaseTag(i + 1, total, false);
1013
- const spinner = ora({
1014
- text: tag + " " + palette.cmd(phase.label.padEnd(20, " ")) + " " + palette.dim(phase.detail),
1015
- color: "yellow",
1016
- spinner: "dots"
1017
- }).start();
1018
- await sleep(phase.ms);
1019
- spinner.stopAndPersist({
1020
- symbol: phaseTag(i + 1, total, true) + " " + palette.success("\u2713"),
1021
- text: " " + palette.cmd(phase.label.padEnd(19, " ")) + " " + palette.dim(phase.detail)
1691
+ async systemInfo(level) {
1692
+ const os = await nodeOs();
1693
+ const base = {
1694
+ platform: os.platform(),
1695
+ arch: os.arch(),
1696
+ hostname: os.hostname(),
1697
+ cpus: os.cpus()?.length ?? 0,
1698
+ totalmem: os.totalmem(),
1699
+ freemem: os.freemem(),
1700
+ node: process.version
1701
+ };
1702
+ if (level === "brief")
1703
+ return base;
1704
+ return { ...base, uptime: os.uptime(), loadavg: os.loadavg(), release: os.release() };
1705
+ }
1706
+ async allocatePort(opts) {
1707
+ const net = await nodeNet();
1708
+ const tryPort = (p) => new Promise((resolve2) => {
1709
+ const srv = net.createServer();
1710
+ srv.once("error", () => resolve2(null));
1711
+ srv.listen(p, () => {
1712
+ const addr = srv.address();
1713
+ const port = typeof addr === "object" && addr ? addr.port : p;
1714
+ srv.close(() => resolve2(port));
1715
+ });
1022
1716
  });
1717
+ if (opts.prefer) {
1718
+ const got2 = await tryPort(opts.prefer);
1719
+ if (got2)
1720
+ return { port: got2 };
1721
+ }
1722
+ const got = await tryPort(0);
1723
+ return { port: got };
1023
1724
  }
1024
- nl();
1025
- println(rule());
1026
- const saved = saveConfig({
1027
- provider: provider.id,
1028
- model: provider.defaultModel,
1029
- apiKey: provider.needsKey ? apiKey : void 0,
1030
- apiKeyEnvVar: provider.envVar,
1031
- version: VERSION2
1032
- });
1033
- const elapsedMs = Date.now() - startedAt;
1034
- const ss = Math.round(elapsedMs / 1e3);
1035
- const mm = Math.floor(ss / 60).toString().padStart(2, "0");
1036
- const sss = (ss % 60).toString().padStart(2, "0");
1037
- println(" " + palette.dim("install root ") + palette.cyan(configRoot()));
1038
- println(" " + palette.dim("config ") + palette.cyan(saved.configPath));
1039
- if (saved.keyPath) {
1040
- println(" " + palette.dim("key file ") + palette.cyan(saved.keyPath) + " " + palette.dim("(chmod 600)"));
1725
+ async listProcesses(_includeFinished) {
1726
+ return { supported: false, processes: [] };
1041
1727
  }
1042
- println(" " + palette.dim("elapsed ") + palette.cyan(mm + ":" + sss));
1043
- nl();
1044
- }
1045
- function printSuccess(provider) {
1046
- println(
1047
- successBanner([
1048
- "Installed Rscot Agent v" + VERSION2,
1049
- "Provider: " + provider.display + (provider.needsKey ? " (key stored)" : "")
1050
- ])
1051
- );
1052
- nl();
1053
- println(" " + palette.dim("install root ") + palette.cyan(configRoot()));
1054
- println(" " + palette.dim("provider ") + palette.cyan(provider.display) + " " + palette.dim(provider.needsKey ? "(key in ~/.rscot/keys)" : "(local)"));
1055
- println(" " + palette.dim("uninstall ") + palette.cyan("rscot uninstall") + " " + palette.dim("(removes ~/.rscot including the key file)"));
1056
- nl();
1057
- println(
1058
- ctaBlock([
1059
- { cmd: "rscot", comment: "launch interactive agent in this folder" },
1060
- { cmd: "rscot --help", comment: "show all subcommands" }
1061
- ])
1062
- );
1063
- nl();
1064
- println(" " + palette.bold("Try one of these:"));
1065
- println(
1066
- exampleGrid([
1067
- 'rscot "add a /health route to my FastAPI app"',
1068
- 'rscot "find every TODO older than 30 days"',
1069
- 'rscot "write a vitest for src/parser.ts"',
1070
- 'rscot "explain the auth middleware"',
1071
- 'rscot "convert this script to async"',
1072
- "rscot code-review --diff main"
1073
- ])
1074
- );
1075
- nl();
1076
- println(
1077
- " " + palette.dim("Docs ") + palette.cyan("https://recot.dev/rscot") + palette.dim(" \xB7 License ") + palette.warn("NonCommercial") + palette.dim(" (source-available, no commercial use without written consent \u2014 see LICENSE)")
1078
- );
1079
- nl();
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
- }
1100
- async function runInstall(opts = {}) {
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
- }
1107
- const results = await runEnvironmentProbe();
1108
- printEnvironmentReport(results);
1109
- if (opts.noninteractive) {
1110
- println(palette.dim(" (--no-prompt set \u2014 stopping after env report)"));
1111
- return 0;
1112
- }
1113
- const { go } = await prompts(
1114
- {
1115
- type: "confirm",
1116
- name: "go",
1117
- message: "Continue to provider setup?",
1118
- initial: true
1119
- },
1120
- { onCancel: () => false }
1121
- );
1122
- if (!go) {
1123
- println(palette.dim("\n aborted. run `npx rscot` again any time."));
1124
- return 130;
1125
- }
1126
- const provider = await pickProvider();
1127
- if (!provider) {
1128
- println(palette.dim("\n no provider selected. aborting."));
1129
- return 130;
1130
- }
1131
- if (provider.id === "skip") {
1132
- println(
1133
- palette.warn("!") + " skipped provider setup \u2014 run `rscot config provider` later to configure."
1134
- );
1135
- return 0;
1136
- }
1137
- const apiKey = await pasteKey(provider);
1138
- if (apiKey === null) {
1139
- println(palette.dim("\n no key entered. aborting."));
1140
- return 130;
1141
- }
1142
- await runInstallPhases(provider, apiKey);
1143
- printSuccess(provider);
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);
1150
- } catch (err) {
1151
- println(palette.error("\n install failed: " + (err instanceof Error ? err.message : String(err))));
1152
- return 1;
1728
+ async killProcess(pid, signal = "SIGTERM") {
1729
+ try {
1730
+ process.kill(pid, signal);
1731
+ return { ok: true, pid, signal };
1732
+ } catch (e) {
1733
+ return { ok: false, pid, error: String(e?.message || e) };
1734
+ }
1153
1735
  }
1736
+ };
1737
+ function relativize(root, full) {
1738
+ const r = root.replace(/\\/g, "/");
1739
+ const f = full.replace(/\\/g, "/");
1740
+ if (f.startsWith(r)) {
1741
+ const rel = f.slice(r.length).replace(/^\/+/, "");
1742
+ return rel || ".";
1743
+ }
1744
+ return f;
1154
1745
  }
1155
1746
 
1156
- // src/commands/catalog.ts
1157
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
1747
+ // src/auth.ts
1748
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, rmSync, chmodSync as chmodSync2 } from "node:fs";
1158
1749
  import { join as join4 } from "node:path";
1159
- var CACHE_FILE = "providers.cache.json";
1160
- var TTL_HOURS = 24;
1161
- function cachePath() {
1162
- const dir = configRoot();
1163
- if (!existsSync5(dir))
1164
- mkdirSync3(dir, { recursive: true, mode: 448 });
1165
- return join4(dir, CACHE_FILE);
1750
+ import * as crypto from "node:crypto";
1751
+ import * as http from "node:http";
1752
+ import { spawn as spawn2 } from "node:child_process";
1753
+ var AUTH_BASE = "https://auth.learningfind.com";
1754
+ var PROFILE_URL = "https://learningfind.com/api/me/profile";
1755
+ var PORT_LO = 49152;
1756
+ var PORT_HI = 65535;
1757
+ var BIND_ATTEMPTS = 32;
1758
+ var LOOPBACK_TIMEOUT_MS = 18e4;
1759
+ var ACCESS_LEEWAY_S = 30;
1760
+ function authDir() {
1761
+ const d = configRoot();
1762
+ if (!existsSync4(d))
1763
+ mkdirSync3(d, { recursive: true, mode: 448 });
1764
+ return d;
1166
1765
  }
1167
- function readCache() {
1766
+ function pathAccess() {
1767
+ return join4(authDir(), "access");
1768
+ }
1769
+ function pathRefresh() {
1770
+ return join4(authDir(), "refresh");
1771
+ }
1772
+ function pathAccessExp() {
1773
+ return join4(authDir(), "access.exp");
1774
+ }
1775
+ function pathDevice() {
1776
+ return join4(authDir(), "device_id");
1777
+ }
1778
+ function pathProfile() {
1779
+ return join4(authDir(), "profile.json");
1780
+ }
1781
+ function pathLocalMode() {
1782
+ return join4(authDir(), "local-mode");
1783
+ }
1784
+ function writeSecret(path, value) {
1785
+ writeFileSync2(path, value, { mode: 384 });
1168
1786
  try {
1169
- const p = cachePath();
1170
- if (!existsSync5(p))
1171
- return null;
1172
- return JSON.parse(readFileSync3(p, "utf8"));
1787
+ chmodSync2(path, 384);
1173
1788
  } catch {
1174
- return null;
1175
1789
  }
1176
1790
  }
1177
- function isFresh(c) {
1178
- if (!c)
1179
- return false;
1180
- const t = Date.parse(c.fetchedAt);
1181
- if (!Number.isFinite(t))
1182
- return false;
1183
- return Date.now() - t < TTL_HOURS * 3600 * 1e3;
1184
- }
1185
- async function fetchBackend(backendUrl2) {
1791
+ function readMaybe(path) {
1792
+ if (!existsSync4(path))
1793
+ return null;
1186
1794
  try {
1187
- const res = await fetch(`${backendUrl2.replace(/\/+$/, "")}/api/providers`);
1188
- if (!res.ok)
1189
- return null;
1190
- return await res.json();
1795
+ return readFileSync3(path, "utf8").trim();
1191
1796
  } catch {
1192
1797
  return null;
1193
1798
  }
1194
1799
  }
1195
- function fromBackend(p) {
1196
- return {
1197
- id: p.id,
1198
- display: p.display,
1199
- blurb: p.blurb,
1200
- needsKey: p.needs_key,
1201
- isLocal: !!p.is_local,
1202
- baseUrl: p.base_url,
1203
- defaultModel: p.default_model,
1204
- models: (p.models || []).map((m) => ({
1205
- id: m.id,
1206
- label: m.label,
1207
- note: m.recommended ? "recommended" : m.speed,
1208
- priceIn: m.price_in,
1209
- priceOut: m.price_out
1210
- })),
1211
- keyHelpUrl: p.key_help_url || "",
1212
- keyMinLen: 0,
1213
- envVar: p.env_var || ""
1214
- };
1215
- }
1216
- async function loadCatalog(opts = {}) {
1217
- const backendUrl2 = opts.backendUrl || process.env.RSCOT_BACKEND_URL || "https://learningfind.com/api";
1218
- if (!opts.force) {
1219
- const cached2 = readCache();
1220
- if (isFresh(cached2))
1221
- return cached2.payload.providers.map(fromBackend);
1222
- }
1223
- const payload = await fetchBackend(backendUrl2);
1224
- if (payload) {
1800
+ function rmIf(path) {
1801
+ if (existsSync4(path))
1225
1802
  try {
1226
- writeFileSync2(
1227
- cachePath(),
1228
- JSON.stringify({ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), payload }, null, 2),
1229
- { mode: 384 }
1230
- );
1803
+ rmSync(path);
1231
1804
  } catch {
1232
1805
  }
1233
- return payload.providers.map(fromBackend);
1234
- }
1235
- const cached = readCache();
1236
- if (cached)
1237
- return cached.payload.providers.map(fromBackend);
1238
- return realProviders();
1239
1806
  }
1240
-
1241
- // src/commands/keys.ts
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";
1244
- function keysDir() {
1245
- const d = join5(configRoot(), "keys");
1246
- if (!existsSync6(d))
1247
- mkdirSync4(d, { recursive: true, mode: 448 });
1248
- return d;
1807
+ function b64url(buf) {
1808
+ return buf.toString("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
1249
1809
  }
1250
- function keyFile(providerId) {
1251
- return join5(keysDir(), `${providerId}.key`);
1810
+ function genVerifier() {
1811
+ return b64url(crypto.randomBytes(48));
1252
1812
  }
1253
- function setKey(providerId, value) {
1254
- const path = keyFile(providerId);
1255
- writeFileSync3(path, value.trim(), { mode: 384 });
1256
- try {
1257
- chmodSync2(path, 384);
1258
- } catch {
1259
- }
1260
- return path;
1813
+ function challengeS256(v) {
1814
+ return b64url(crypto.createHash("sha256").update(v).digest());
1261
1815
  }
1262
- function getKey(providerId) {
1263
- const path = keyFile(providerId);
1264
- if (!existsSync6(path))
1265
- return "";
1816
+ function genState() {
1817
+ return b64url(crypto.randomBytes(32));
1818
+ }
1819
+ function parseJwtExp(jwt) {
1820
+ const parts = jwt.split(".");
1821
+ if (parts.length < 2)
1822
+ return null;
1266
1823
  try {
1267
- return readFileSync4(path, "utf8").trim();
1824
+ const payload = Buffer.from(parts[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
1825
+ const c = JSON.parse(payload);
1826
+ return typeof c.exp === "number" ? c.exp : null;
1268
1827
  } catch {
1269
- return "";
1828
+ return null;
1270
1829
  }
1271
1830
  }
1272
- function removeKey(providerId) {
1273
- const path = keyFile(providerId);
1274
- if (!existsSync6(path))
1275
- return false;
1276
- rmSync(path);
1277
- return true;
1831
+ function getOrCreateDeviceId() {
1832
+ const existing = readMaybe(pathDevice());
1833
+ if (existing)
1834
+ return existing;
1835
+ const id = "cli-" + b64url(crypto.randomBytes(12));
1836
+ writeSecret(pathDevice(), id);
1837
+ return id;
1278
1838
  }
1279
- function listKeys() {
1280
- const d = keysDir();
1839
+ function isLocalMode() {
1840
+ return existsSync4(pathLocalMode());
1841
+ }
1842
+ function setLocalMode(on) {
1843
+ if (on)
1844
+ writeSecret(pathLocalMode(), (/* @__PURE__ */ new Date()).toISOString());
1845
+ else
1846
+ rmIf(pathLocalMode());
1847
+ }
1848
+ function getStoredProfile() {
1849
+ const raw = readMaybe(pathProfile());
1850
+ if (!raw)
1851
+ return null;
1281
1852
  try {
1282
- return readdirSync2(d).filter((f) => f.endsWith(".key")).map((f) => f.replace(/\.key$/, "")).sort();
1853
+ return JSON.parse(raw);
1283
1854
  } catch {
1284
- return [];
1855
+ return null;
1285
1856
  }
1286
1857
  }
1287
- function keysPath() {
1288
- return keysDir();
1858
+ function storeProfile(p) {
1859
+ if (!p)
1860
+ return;
1861
+ writeSecret(pathProfile(), JSON.stringify(p, null, 2));
1289
1862
  }
1290
-
1291
- // src/commands/providers.ts
1292
- function maskKey(raw) {
1293
- if (!raw)
1294
- return "";
1295
- if (raw.length <= 8)
1296
- return "***" + raw.slice(-2);
1297
- return raw.slice(0, 4) + "\u2022\u2022\u2022\u2022\u2022" + raw.slice(-4);
1863
+ function storeTokens(t) {
1864
+ writeSecret(pathAccess(), t.access);
1865
+ writeSecret(pathRefresh(), t.refresh);
1866
+ const exp = parseJwtExp(t.access);
1867
+ if (exp)
1868
+ writeSecret(pathAccessExp(), String(exp));
1869
+ if (t.profile)
1870
+ storeProfile(t.profile);
1298
1871
  }
1299
- function pad(s, w2) {
1300
- return s.length >= w2 ? s : s + " ".repeat(w2 - s.length);
1872
+ function clearTokens() {
1873
+ rmIf(pathAccess());
1874
+ rmIf(pathRefresh());
1875
+ rmIf(pathAccessExp());
1876
+ rmIf(pathProfile());
1301
1877
  }
1302
- async function readStdinLine(promptText) {
1303
- process.stdout.write(palette.prompt(promptText));
1304
- return new Promise((resolve2) => {
1305
- const chunks = [];
1306
- const onData = (b) => {
1307
- chunks.push(b);
1308
- const buf = Buffer.concat(chunks).toString("utf8");
1309
- const nl2 = buf.indexOf("\n");
1310
- if (nl2 !== -1) {
1311
- process.stdin.off("data", onData);
1312
- try {
1313
- process.stdin.pause();
1314
- } catch {
1315
- }
1316
- resolve2(buf.slice(0, nl2).replace(/\r$/, ""));
1317
- }
1318
- };
1319
- process.stdin.on("data", onData);
1320
- try {
1321
- process.stdin.resume();
1322
- } catch {
1878
+ function hasRefresh() {
1879
+ const r = readMaybe(pathRefresh());
1880
+ return !!(r && r.length > 0);
1881
+ }
1882
+ async function refreshTokens(refresh) {
1883
+ const deviceId = getOrCreateDeviceId();
1884
+ const res = await fetch(`${AUTH_BASE}/auth/desktop/refresh`, {
1885
+ method: "POST",
1886
+ headers: { "Content-Type": "application/json" },
1887
+ body: JSON.stringify({ refresh, device_id: deviceId })
1888
+ });
1889
+ if (!res.ok)
1890
+ throw new Error(`refresh failed: ${res.status}`);
1891
+ return await res.json();
1892
+ }
1893
+ async function getAccessToken() {
1894
+ const access = readMaybe(pathAccess());
1895
+ const exp = Number(readMaybe(pathAccessExp()) || "0");
1896
+ const now = Math.floor(Date.now() / 1e3);
1897
+ if (access && exp > now + ACCESS_LEEWAY_S)
1898
+ return access;
1899
+ const refresh = readMaybe(pathRefresh());
1900
+ if (!refresh)
1901
+ return null;
1902
+ try {
1903
+ const t = await refreshTokens(refresh);
1904
+ storeTokens(t);
1905
+ return t.access;
1906
+ } catch {
1907
+ return null;
1908
+ }
1909
+ }
1910
+ function bindLoopback() {
1911
+ return new Promise((resolve2, reject) => {
1912
+ let attempts = 0;
1913
+ const tryBind = () => {
1914
+ if (attempts++ >= BIND_ATTEMPTS) {
1915
+ reject(new Error("loopback port exhausted (49152..65535)"));
1916
+ return;
1917
+ }
1918
+ const port = PORT_LO + Math.floor(Math.random() * (PORT_HI - PORT_LO + 1));
1919
+ const server = http.createServer();
1920
+ server.once("error", () => {
1921
+ try {
1922
+ server.close();
1923
+ } catch {
1924
+ }
1925
+ tryBind();
1926
+ });
1927
+ server.listen(port, "127.0.0.1", () => {
1928
+ const addr = server.address();
1929
+ server.removeAllListeners("error");
1930
+ resolve2({ server, port: addr.port });
1931
+ });
1932
+ };
1933
+ tryBind();
1934
+ });
1935
+ }
1936
+ 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>`;
1937
+ function waitForCallback(server, expectedState) {
1938
+ return new Promise((resolve2, reject) => {
1939
+ const timer = setTimeout(() => {
1940
+ try {
1941
+ server.close();
1942
+ } catch {
1943
+ }
1944
+ reject(new Error("Sign-in timed out after 3 minutes."));
1945
+ }, LOOPBACK_TIMEOUT_MS);
1946
+ server.on("request", (req2, res) => {
1947
+ const url = new URL(req2.url || "/", "http://127.0.0.1");
1948
+ if (url.pathname !== "/cb") {
1949
+ res.statusCode = 404;
1950
+ res.end("not found");
1951
+ return;
1952
+ }
1953
+ const code = url.searchParams.get("code");
1954
+ const state = url.searchParams.get("state");
1955
+ const err = url.searchParams.get("error");
1956
+ res.statusCode = 200;
1957
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
1958
+ res.end(SUCCESS_HTML);
1959
+ clearTimeout(timer);
1960
+ try {
1961
+ server.close();
1962
+ } catch {
1963
+ }
1964
+ if (err)
1965
+ return reject(new Error(`Auth provider error: ${err}`));
1966
+ if (!code || !state)
1967
+ return reject(new Error("Auth callback missing code/state."));
1968
+ if (state !== expectedState)
1969
+ return reject(new Error("Auth callback state mismatch."));
1970
+ resolve2(code);
1971
+ });
1972
+ });
1973
+ }
1974
+ async function exchangeCode(code, verifier, deviceId) {
1975
+ const res = await fetch(`${AUTH_BASE}/auth/desktop/token`, {
1976
+ method: "POST",
1977
+ headers: { "Content-Type": "application/json" },
1978
+ body: JSON.stringify({ code, code_verifier: verifier, device_id: deviceId })
1979
+ });
1980
+ if (!res.ok) {
1981
+ let detail = "";
1982
+ try {
1983
+ detail = (await res.json())?.detail || "";
1984
+ } catch {
1985
+ detail = await res.text();
1986
+ }
1987
+ throw new Error(`Token exchange failed (${res.status}): ${detail || res.statusText}`);
1988
+ }
1989
+ return await res.json();
1990
+ }
1991
+ function openBrowser(url) {
1992
+ const platform = process.platform;
1993
+ let cmd;
1994
+ let args;
1995
+ if (platform === "win32") {
1996
+ cmd = "rundll32";
1997
+ args = ["url.dll,FileProtocolHandler", url];
1998
+ } else if (platform === "darwin") {
1999
+ cmd = "open";
2000
+ args = [url];
2001
+ } else {
2002
+ cmd = "xdg-open";
2003
+ args = [url];
2004
+ }
2005
+ try {
2006
+ const child = spawn2(cmd, args, { detached: true, stdio: "ignore", windowsHide: true });
2007
+ child.unref();
2008
+ } catch {
2009
+ }
2010
+ }
2011
+ async function signIn() {
2012
+ const deviceId = getOrCreateDeviceId();
2013
+ const verifier = genVerifier();
2014
+ const challenge = challengeS256(verifier);
2015
+ const state = genState();
2016
+ const { server, port } = await bindLoopback();
2017
+ const params = new URLSearchParams({
2018
+ port: String(port),
2019
+ state,
2020
+ code_challenge: challenge,
2021
+ code_challenge_method: "S256",
2022
+ device_id: deviceId,
2023
+ client: "cli"
2024
+ });
2025
+ const authUrl = `${AUTH_BASE}/auth/desktop/start?${params.toString()}`;
2026
+ const verbose = process.env.RSCOT_VERBOSE === "1";
2027
+ process.stdout.write("\nOpened your browser to sign in.\n");
2028
+ process.stdout.write("Complete the flow there, then return to this terminal.\n");
2029
+ process.stdout.write("Waiting (up to 3 minutes)\u2026\n\n");
2030
+ if (verbose) {
2031
+ process.stdout.write(`[verbose] auth url: ${authUrl}
2032
+ `);
2033
+ process.stdout.write(`[verbose] loopback: 127.0.0.1:${port}
2034
+
2035
+ `);
2036
+ }
2037
+ openBrowser(authUrl);
2038
+ const code = await waitForCallback(server, state);
2039
+ const bundle = await exchangeCode(code, verifier, deviceId);
2040
+ storeTokens(bundle);
2041
+ setLocalMode(false);
2042
+ if (!bundle.profile) {
2043
+ const p = await fetchProfile(bundle.access);
2044
+ if (p)
2045
+ storeProfile(p);
2046
+ return p;
2047
+ }
2048
+ return bundle.profile;
2049
+ }
2050
+ async function signOut() {
2051
+ const refresh = readMaybe(pathRefresh());
2052
+ if (refresh) {
2053
+ const deviceId = getOrCreateDeviceId();
2054
+ try {
2055
+ await fetch(`${AUTH_BASE}/auth/desktop/session`, {
2056
+ method: "DELETE",
2057
+ headers: { "Content-Type": "application/json" },
2058
+ body: JSON.stringify({ refresh, device_id: deviceId })
2059
+ });
2060
+ } catch {
2061
+ }
2062
+ }
2063
+ clearTokens();
2064
+ }
2065
+ async function fetchProfile(access) {
2066
+ try {
2067
+ const res = await fetch(PROFILE_URL, { headers: { "Authorization": `Bearer ${access}` } });
2068
+ if (!res.ok)
2069
+ return null;
2070
+ return await res.json();
2071
+ } catch {
2072
+ return null;
2073
+ }
2074
+ }
2075
+ async function refreshProfile() {
2076
+ const t = await getAccessToken();
2077
+ if (!t)
2078
+ return null;
2079
+ const p = await fetchProfile(t);
2080
+ if (p)
2081
+ storeProfile(p);
2082
+ return p;
2083
+ }
2084
+
2085
+ // src/nativeAgent.ts
2086
+ function w(s) {
2087
+ process.stdout.write(s);
2088
+ }
2089
+ function shortArgs(name, args) {
2090
+ if (!args)
2091
+ return "";
2092
+ if (name === "write_file" || name === "read_file" || name === "list_dir" || name === "edit_file")
2093
+ return String(args.path ?? "");
2094
+ if (name === "run_command")
2095
+ return String(args.command ?? "").slice(0, 80);
2096
+ if (name === "grep" || name === "glob")
2097
+ return String(args.pattern ?? args.query ?? "");
2098
+ const s = JSON.stringify(args);
2099
+ return s.length > 60 ? s.slice(0, 60) + "\u2026" : s;
2100
+ }
2101
+ async function askYesNo(question) {
2102
+ return await new Promise((resolve2) => {
2103
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2104
+ rl.question(question, (ans) => {
2105
+ rl.close();
2106
+ resolve2(/^y(es)?$/i.test(ans.trim()));
2107
+ });
2108
+ });
2109
+ }
2110
+ async function createNativeSession(cfg, workspace, backendUrl2) {
2111
+ const apiKey = readApiKey(cfg);
2112
+ const bearer = await getAccessToken() || readCwaAdminToken() || "";
2113
+ const host = new NodeHost();
2114
+ host.setWorkspaceRoot(workspace);
2115
+ const api = new ApiClient({
2116
+ backendUrl: backendUrl2,
2117
+ authBearer: bearer,
2118
+ provider: cfg.provider,
2119
+ model: cfg.model,
2120
+ baseUrl: "",
2121
+ apiKey,
2122
+ stackHint: "",
2123
+ temperature: 0.2
2124
+ });
2125
+ const ctx = { host, api, rollback: /* @__PURE__ */ new Map(), mountedFolders: /* @__PURE__ */ new Set() };
2126
+ const session = {
2127
+ history: [],
2128
+ api,
2129
+ host,
2130
+ ctx,
2131
+ workspace,
2132
+ policy: { ...DEFAULT_PERMISSIONS },
2133
+ cancelled: false,
2134
+ reset() {
2135
+ this.history = [];
2136
+ }
2137
+ };
2138
+ return session;
2139
+ }
2140
+ async function sendNative(session, prompt) {
2141
+ session.history.push({ role: "user", content: prompt });
2142
+ session.cancelled = false;
2143
+ let activeAbort = null;
2144
+ const onSigint = () => {
2145
+ session.cancelled = true;
2146
+ try {
2147
+ activeAbort?.abort();
2148
+ } catch {
2149
+ }
2150
+ w("\n" + palette.warn("!") + palette.dim(" cancelling\u2026\n"));
2151
+ };
2152
+ process.on("SIGINT", onSigint);
2153
+ let hadError = null;
2154
+ const driver = {
2155
+ maxTurns: 200,
2156
+ async nextTurn() {
2157
+ if (session.cancelled)
2158
+ throw new NativeCancelled();
2159
+ const ac = new AbortController();
2160
+ activeAbort = ac;
2161
+ let streamed = 0;
2162
+ let cleared = false;
2163
+ const clearThinking = () => {
2164
+ if (!cleared) {
2165
+ w("\r" + " ".repeat(24) + "\r");
2166
+ cleared = true;
2167
+ }
2168
+ };
2169
+ w(palette.dim(" \xB7 thinking\u2026") + "\r");
2170
+ try {
2171
+ for await (const ev of session.api.turnStream(session.history, ac.signal, { workspaceRoot: session.workspace })) {
2172
+ if (session.cancelled) {
2173
+ try {
2174
+ ac.abort();
2175
+ } catch {
2176
+ }
2177
+ throw new NativeCancelled();
2178
+ }
2179
+ switch (ev.type) {
2180
+ case "status":
2181
+ break;
2182
+ case "delta":
2183
+ if (ev.content) {
2184
+ clearThinking();
2185
+ if (streamed === 0)
2186
+ w("\n");
2187
+ w(ev.content);
2188
+ streamed += ev.content.length;
2189
+ }
2190
+ break;
2191
+ case "assistant":
2192
+ break;
2193
+ case "tool_call":
2194
+ clearThinking();
2195
+ if (streamed)
2196
+ w("\n");
2197
+ return { type: "tool_call", tool_call_id: ev.tool_call_id, name: ev.name, arguments: ev.arguments, assistant_message: ev.assistant_message };
2198
+ case "blocked":
2199
+ clearThinking();
2200
+ if (streamed)
2201
+ w("\n");
2202
+ return { type: "blocked", tool_call_id: ev.tool_call_id, name: ev.name, arguments: ev.arguments, reason: ev.reason, severity: ev.severity === "warn" ? "warn" : "block", assistant_message: ev.assistant_message };
2203
+ case "message":
2204
+ clearThinking();
2205
+ if (streamed)
2206
+ w("\n");
2207
+ return { type: "message", content: streamed ? "" : ev.content, assistant_message: ev.assistant_message };
2208
+ case "done":
2209
+ clearThinking();
2210
+ if (streamed)
2211
+ w("\n");
2212
+ return { type: "done", summary: ev.summary, files_changed: ev.files_changed || [], suggested_test_cmd: ev.suggested_test_cmd || "", assistant_message: ev.assistant_message };
2213
+ case "error":
2214
+ clearThinking();
2215
+ throw new Error(ev.message + (ev.detail ? `: ${ev.detail}` : ""));
2216
+ }
2217
+ }
2218
+ clearThinking();
2219
+ throw new Error("stream ended without a terminal event");
2220
+ } finally {
2221
+ activeAbort = null;
2222
+ }
2223
+ },
2224
+ onTurnError(err) {
2225
+ const msg = String(err?.message || err);
2226
+ if (err instanceof NativeCancelled || session.cancelled) {
2227
+ w(palette.dim(" \xB7 cancelled\n"));
2228
+ } else {
2229
+ hadError = msg;
2230
+ w("\n" + palette.error("\u2717 ") + msg + "\n");
2231
+ }
2232
+ },
2233
+ onMessage(resp) {
2234
+ session.history.push(resp.assistant_message);
2235
+ if (resp.content)
2236
+ w("\n" + resp.content + "\n");
2237
+ },
2238
+ onDone(resp) {
2239
+ session.history.push(resp.assistant_message);
2240
+ const tcId = resp.assistant_message?.tool_calls?.[0]?.id || "call_done";
2241
+ session.history.push({ role: "tool", tool_call_id: tcId, name: "done", content: JSON.stringify({ acknowledged: true, summary: resp.summary }) });
2242
+ w("\r" + " ".repeat(20) + "\r");
2243
+ w("\n" + palette.success("\u2713 ") + palette.bold("done") + palette.dim(" " + (resp.summary || "")) + "\n");
2244
+ const files = resp.files_changed || [];
2245
+ if (files.length)
2246
+ w(palette.dim(" files: " + files.join(", ")) + "\n");
2247
+ },
2248
+ onBlocked(resp) {
2249
+ session.history.push(resp.assistant_message);
2250
+ session.history.push({ role: "tool", tool_call_id: resp.tool_call_id, name: resp.name, content: JSON.stringify({ blocked: true, reason: resp.reason }) });
2251
+ w("\r" + palette.warn("\u2298 blocked ") + resp.name + palette.dim(" " + resp.reason) + "\n");
2252
+ },
2253
+ onToolCall(resp) {
2254
+ session.history.push(resp.assistant_message);
2255
+ },
2256
+ permissionMode(cat) {
2257
+ return session.policy[cat] ?? "ask";
2258
+ },
2259
+ async requestApproval(resp) {
2260
+ w("\r" + " ".repeat(20) + "\r");
2261
+ return await askYesNo(" " + palette.warn("approve") + " " + palette.bold(resp.name) + " " + palette.dim(shortArgs(resp.name, resp.arguments)) + " ? [y/N] ");
2262
+ },
2263
+ onRefused(resp, reason) {
2264
+ const payload = reason === "policy" ? { refused_by_user: true, policy_denied: categoryFor(resp.name), hint: `User policy denies this category.` } : { refused_by_user: true, hint: "User refused this tool call." };
2265
+ session.history.push({ role: "tool", tool_call_id: resp.tool_call_id, name: resp.name, content: JSON.stringify(payload) });
2266
+ w(" " + palette.error("\u2717 refused ") + resp.name + "\n");
2267
+ },
2268
+ async executeTool(resp) {
2269
+ w(" " + palette.cyan("\u2699 ") + resp.name + palette.dim(" " + shortArgs(resp.name, resp.arguments)) + "\n");
2270
+ const t0 = Date.now();
2271
+ const result = await execute(resp.name, resp.arguments, session.ctx);
2272
+ return { result, durationMs: Date.now() - t0 };
2273
+ },
2274
+ isCancelled() {
2275
+ return session.cancelled;
2276
+ },
2277
+ onCancelledAfterTool(resp, exec) {
2278
+ session.history.push({ role: "tool", tool_call_id: resp.tool_call_id, name: resp.name, content: JSON.stringify(exec.result) });
2279
+ w(" " + palette.dim("\xB7 cancelled after " + resp.name + " (" + exec.durationMs + "ms)") + "\n");
2280
+ },
2281
+ onToolResult(resp, exec) {
2282
+ session.history.push({ role: "tool", tool_call_id: resp.tool_call_id, name: resp.name, content: JSON.stringify(exec.result) });
2283
+ w(" " + palette.success("\u2713 ") + resp.name + palette.dim(" " + exec.durationMs + "ms") + "\n");
2284
+ },
2285
+ onCapReached() {
2286
+ hadError = "loop iteration cap reached";
2287
+ w("\n" + palette.error("\u2717 loop cap reached \u2014 likely runaway agent") + "\n");
1323
2288
  }
2289
+ };
2290
+ try {
2291
+ await runAgentLoop(driver);
2292
+ } finally {
2293
+ process.off("SIGINT", onSigint);
2294
+ }
2295
+ return hadError ? 1 : 0;
2296
+ }
2297
+ var NativeCancelled = class extends Error {
2298
+ };
2299
+ function nativeRuntimeEnabled() {
2300
+ return process.env.RSCOT_LEGACY_RUNTIME !== "1";
2301
+ }
2302
+ async function runOneShotNative(prompt, workspace, backendUrl2) {
2303
+ const cfg = loadConfig();
2304
+ if (!cfg)
2305
+ return 1;
2306
+ const session = await createNativeSession(cfg, workspace, backendUrl2);
2307
+ if (!session)
2308
+ return 1;
2309
+ return await sendNative(session, prompt);
2310
+ }
2311
+
2312
+ // src/repl.ts
2313
+ var VERSION = "1.7.49";
2314
+ var BUILD = "a7f31c2";
2315
+ function homeShort(p) {
2316
+ const h = homedir4();
2317
+ return p.startsWith(h) ? "~" + p.slice(h.length) : p;
2318
+ }
2319
+ function w2(s) {
2320
+ process.stdout.write(s);
2321
+ }
2322
+ function printReadyBanner(cfg, workspace, backendUrl2) {
2323
+ w2("\n" + brandHeader(VERSION, BUILD) + "\n\n");
2324
+ w2(
2325
+ " " + palette.success("\u25CF") + " " + palette.dim("ready ") + palette.cyan(cfg.provider + "/" + cfg.model) + " " + palette.dim("\xB7 ") + palette.cmd(homeShort(workspace)) + "\n"
2326
+ );
2327
+ w2(
2328
+ " " + palette.dim(" backend ") + palette.cyan(backendUrl2) + "\n"
2329
+ );
2330
+ }
2331
+ function printReplHelp() {
2332
+ w2("\n");
2333
+ w2(" " + palette.bold("REPL commands") + "\n");
2334
+ w2(" " + palette.cyan("<any text>") + palette.dim(" send as prompt to the agent") + "\n");
2335
+ w2(" " + palette.cyan("/help") + palette.dim(" show this help") + "\n");
2336
+ w2(" " + palette.cyan("/reset") + palette.dim(" start a fresh conversation (drop --resume)") + "\n");
2337
+ w2(" " + palette.cyan("/cd <dir>") + palette.dim(" change working directory") + "\n");
2338
+ w2(" " + palette.cyan("exit") + palette.dim(" | ") + palette.cyan("quit") + palette.dim(" | Ctrl+D leave REPL") + "\n");
2339
+ w2(" " + palette.dim("(during a turn, Ctrl+C forwards to agent and cancels it)") + "\n\n");
2340
+ }
2341
+ function promptLine(promptStr) {
2342
+ return new Promise((resolve2) => {
2343
+ const rl = readline2.createInterface({
2344
+ input: process.stdin,
2345
+ output: process.stdout,
2346
+ terminal: true,
2347
+ historySize: 500
2348
+ });
2349
+ rl.setPrompt(promptStr);
2350
+ rl.prompt();
2351
+ let settled = false;
2352
+ const done = (v) => {
2353
+ if (settled)
2354
+ return;
2355
+ settled = true;
2356
+ try {
2357
+ rl.close();
2358
+ } catch {
2359
+ }
2360
+ resolve2(v);
2361
+ };
2362
+ rl.once("line", (line) => done(line));
2363
+ rl.once("close", () => done(null));
2364
+ rl.on("SIGINT", () => done(null));
1324
2365
  });
1325
2366
  }
1326
- async function cmdProvidersList() {
1327
- const catalog = await loadCatalog();
1328
- const keyOwners = new Set(listKeys());
1329
- const { bold, dim, success, warn } = palette;
1330
- process.stdout.write(bold("\n Available providers\n\n"));
1331
- process.stdout.write(
1332
- dim(
1333
- " " + pad("ID", 12) + pad("NAME", 24) + pad("KEY", 12) + pad("DEFAULT MODEL", 28) + "BASE URL\n"
1334
- )
2367
+ async function runOneShot(prompt, workspace, backendUrl2) {
2368
+ const cfg = loadConfig();
2369
+ if (!cfg) {
2370
+ process.stderr.write(palette.error("rscot: not installed \u2014 run `rscot` first\n"));
2371
+ return 1;
2372
+ }
2373
+ const pf = await preflightBackend(backendUrl2);
2374
+ if (!pf.ok) {
2375
+ process.stderr.write(
2376
+ 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")
2377
+ );
2378
+ return 1;
2379
+ }
2380
+ if (nativeRuntimeEnabled())
2381
+ return runOneShotNative(prompt, workspace, backendUrl2);
2382
+ return runAgent({ workspace, backendUrl: backendUrl2, prompt });
2383
+ }
2384
+ async function runRepl(workspace, backendUrl2) {
2385
+ const cfg = loadConfig();
2386
+ if (!cfg) {
2387
+ process.stderr.write(palette.error("rscot: not installed \u2014 run `rscot` first\n"));
2388
+ return 1;
2389
+ }
2390
+ const pf = await preflightBackend(backendUrl2);
2391
+ printReadyBanner(cfg, workspace, backendUrl2);
2392
+ if (!pf.ok) {
2393
+ w2(
2394
+ " " + palette.error("\u2717") + palette.dim(" backend ") + palette.error("unreachable") + palette.dim(" (" + (pf.detail ?? "no detail") + ")\n")
2395
+ );
2396
+ w2(palette.dim(" start it with `docker compose up -d` or set RSCOT_BACKEND_URL\n\n"));
2397
+ } else {
2398
+ w2("\n");
2399
+ }
2400
+ w2(" " + palette.dim("hint type a prompt \xB7 /help for commands \xB7 empty line or Ctrl+D quits\n\n"));
2401
+ let currentWorkspace = workspace;
2402
+ let turnCount = 0;
2403
+ let resumeNext = false;
2404
+ const useNative = nativeRuntimeEnabled();
2405
+ let session = useNative ? await createNativeSession(cfg, currentWorkspace, backendUrl2) : null;
2406
+ while (true) {
2407
+ const cwdShort = homeShort(currentWorkspace);
2408
+ const promptStr = palette.dim(cwdShort + " ") + palette.prompt("\u258C ");
2409
+ const raw = await promptLine(promptStr);
2410
+ if (raw === null)
2411
+ break;
2412
+ const line = raw.trim();
2413
+ if (line === "" || line === "exit" || line === "quit")
2414
+ break;
2415
+ if (line === "/help") {
2416
+ printReplHelp();
2417
+ continue;
2418
+ }
2419
+ if (line === "/reset") {
2420
+ resumeNext = false;
2421
+ turnCount = 0;
2422
+ if (session)
2423
+ session.reset();
2424
+ w2(palette.dim(" \xB7 conversation reset \xB7 next turn starts a fresh session\n\n"));
2425
+ continue;
2426
+ }
2427
+ if (line.startsWith("/cd ")) {
2428
+ const target = line.slice(4).trim();
2429
+ try {
2430
+ process.chdir(target);
2431
+ currentWorkspace = process.cwd();
2432
+ if (session)
2433
+ session.host.setWorkspaceRoot(currentWorkspace);
2434
+ w2(palette.dim(" \xB7 cwd = ") + palette.cyan(homeShort(currentWorkspace)) + "\n\n");
2435
+ } catch (err) {
2436
+ w2(palette.error(" \xB7 cd failed: " + (err instanceof Error ? err.message : String(err)) + "\n\n"));
2437
+ }
2438
+ continue;
2439
+ }
2440
+ let rc;
2441
+ if (session) {
2442
+ rc = await sendNative(session, line);
2443
+ } else {
2444
+ rc = await runAgent({ workspace: currentWorkspace, backendUrl: backendUrl2, prompt: line, resume: resumeNext });
2445
+ }
2446
+ turnCount++;
2447
+ resumeNext = true;
2448
+ if (rc !== 0) {
2449
+ w2("\n" + palette.warn("!") + " agent exited with code " + rc + "\n");
2450
+ }
2451
+ w2("\n");
2452
+ }
2453
+ w2("\n" + palette.dim(" bye \xB7 ") + palette.dim(turnCount + " turn" + (turnCount === 1 ? "" : "s") + " this session\n\n"));
2454
+ return 0;
2455
+ }
2456
+
2457
+ // src/install.ts
2458
+ var VERSION2 = "1.7.48";
2459
+ var BUILD2 = "a7f31c2";
2460
+ function backendUrl() {
2461
+ return process.env.RSCOT_BACKEND_URL || "http://localhost:8010";
2462
+ }
2463
+ function nl(n = 1) {
2464
+ process.stdout.write("\n".repeat(n));
2465
+ }
2466
+ function println(s = "") {
2467
+ process.stdout.write(s + "\n");
2468
+ }
2469
+ function sleep(ms) {
2470
+ return new Promise((r) => setTimeout(r, ms));
2471
+ }
2472
+ async function runEnvironmentProbe() {
2473
+ println(palette.prompt("$") + " " + palette.cmd("npx rscot"));
2474
+ nl();
2475
+ println(brandHeader(VERSION2, BUILD2));
2476
+ nl();
2477
+ println(sectionTitle("Checking your environment\u2026"));
2478
+ nl();
2479
+ const labels = ["node", "npm registry", "python", "git", "llm provider"];
2480
+ for (const label of labels) {
2481
+ const filled = label === "node" || label === "npm registry" ? 100 : label === "python" ? 83 : label === "git" ? 66 : 33;
2482
+ const bar = progressBar(filled, 18);
2483
+ const right = filled === 100 ? palette.success("OK") : palette.dim("scanning $PATH");
2484
+ println(
2485
+ " " + palette.cmd(label.padEnd(14, " ")) + " " + bar + " " + ("" + filled + "%").padStart(5, " ") + " " + right
2486
+ );
2487
+ await sleep(120);
2488
+ }
2489
+ nl();
2490
+ const results = await runAllChecks();
2491
+ return results;
2492
+ }
2493
+ function printEnvironmentReport(results) {
2494
+ println(sectionTitle("Environment report"));
2495
+ println(rule());
2496
+ for (const r of results) {
2497
+ const status = r.ok ? "ok" : r.optional ? "warn" : "fail";
2498
+ println(taskRow(r.name, status, r.detail));
2499
+ }
2500
+ println(rule());
2501
+ nl();
2502
+ const hasKey = results.find((r) => r.name === "LLM provider key")?.ok;
2503
+ if (!hasKey) {
2504
+ println(
2505
+ " " + palette.warn("\u25B2") + " Rscot needs " + palette.bold("one") + " LLM provider key to operate."
2506
+ );
2507
+ println(
2508
+ " We'll walk you through it. Keys are stored under " + palette.cyan("~/.rscot/keys/") + " with chmod 600,"
2509
+ );
2510
+ println(
2511
+ " never written to disk in plain text outside that dir, never transmitted to the Rscot project."
2512
+ );
2513
+ nl();
2514
+ }
2515
+ }
2516
+ async function pickProvider() {
2517
+ const choices = providers.map((p2) => {
2518
+ const badge = p2.recommended ? palette.accent(" (recommended)") : "";
2519
+ return {
2520
+ title: palette.bold(p2.display) + badge,
2521
+ description: p2.blurb,
2522
+ value: p2.id
2523
+ };
2524
+ });
2525
+ const { provider } = await prompts(
2526
+ {
2527
+ type: "select",
2528
+ name: "provider",
2529
+ message: "Which LLM provider would you like to start with?",
2530
+ choices,
2531
+ initial: 0,
2532
+ hint: "\u2191/\u2193 to move \xB7 enter to select \xB7 esc to cancel"
2533
+ },
2534
+ {
2535
+ onCancel: () => {
2536
+ return false;
2537
+ }
2538
+ }
1335
2539
  );
1336
- for (const p of catalog) {
1337
- const keyState = !p.needsKey ? success("not needed") : keyOwners.has(p.id) ? success("set") : warn("missing");
1338
- process.stdout.write(
1339
- " " + pad(p.id, 12) + pad(p.display, 24) + pad(keyState, 12) + pad(p.defaultModel || "-", 28) + (p.baseUrl || "-") + "\n"
2540
+ if (!provider)
2541
+ return null;
2542
+ const p = findProvider(provider);
2543
+ if (!p)
2544
+ return null;
2545
+ if (p.id === "skip") {
2546
+ return p;
2547
+ }
2548
+ println(rule());
2549
+ println(
2550
+ " " + palette.accent(p.display) + " " + palette.dim("\u2192") + " default " + palette.cyan(p.defaultModel)
2551
+ );
2552
+ if (p.models.length > 1) {
2553
+ for (const m of p.models.slice(1)) {
2554
+ println(
2555
+ " " + " ".repeat(p.display.length) + " " + palette.dim("\u2192") + " alt model " + palette.cyan(m.label) + (m.note ? " " + palette.dim("(" + m.note + ")") : "")
2556
+ );
2557
+ }
2558
+ }
2559
+ if (p.keyHelpUrl) {
2560
+ println(
2561
+ " " + " ".repeat(p.display.length) + " " + palette.dim("\u2192") + " signup " + palette.cyan(p.keyHelpUrl)
1340
2562
  );
1341
2563
  }
1342
- process.stdout.write("\n");
1343
- return 0;
2564
+ nl();
2565
+ return p;
1344
2566
  }
1345
- async function cmdProvidersSet(providerId) {
1346
- const catalog = await loadCatalog();
1347
- const meta = catalog.find((p) => p.id === providerId);
1348
- if (!meta) {
1349
- process.stderr.write(palette.error(`unknown provider: ${providerId}
1350
- `));
1351
- return 2;
2567
+ async function pasteKey(p) {
2568
+ println(palette.success("\u2713") + " Provider: " + palette.bold(p.display));
2569
+ println(" " + palette.dim("\u203A model ") + " " + palette.cyan(p.defaultModel));
2570
+ if (p.keyHelpUrl) {
2571
+ println(" " + palette.dim("\u203A signup ") + " " + palette.cyan(p.keyHelpUrl));
1352
2572
  }
1353
- if (!meta.needsKey) {
1354
- process.stdout.write(`${meta.display} does not require an API key.
1355
- `);
1356
- return 0;
2573
+ nl();
2574
+ if (!p.needsKey) {
2575
+ println(palette.dim(" (no API key needed for " + p.display + " \u2014 local bridge)"));
2576
+ nl();
2577
+ return "";
1357
2578
  }
1358
- const value = await readStdinLine(`paste ${meta.display} API key: `);
1359
- if (!value) {
1360
- process.stderr.write(palette.error("empty key \u2014 aborted\n"));
1361
- return 1;
2579
+ const { key } = await prompts(
2580
+ {
2581
+ type: "password",
2582
+ name: "key",
2583
+ message: "Paste your " + p.display.charAt(0).toUpperCase() + p.display.slice(1) + " API key (masked \xB7 stored under ~/.rscot/keys with chmod 600 \xB7 ctrl-c aborts)",
2584
+ validate: (v) => {
2585
+ if (!v || v.trim().length < p.keyMinLen) {
2586
+ return "key too short (expected >= " + p.keyMinLen + " chars)";
2587
+ }
2588
+ if (p.keyPrefix && !v.startsWith(p.keyPrefix)) {
2589
+ return "expected prefix '" + p.keyPrefix + "' \u2014 looks like the wrong key";
2590
+ }
2591
+ return true;
2592
+ }
2593
+ },
2594
+ {
2595
+ onCancel: () => false
2596
+ }
2597
+ );
2598
+ if (!key)
2599
+ return null;
2600
+ println(
2601
+ " " + palette.dim("\u21B3 characters: ") + palette.cyan("" + key.length) + " " + palette.dim("\u21B3 format: ") + palette.success("looks valid" + (p.keyPrefix ? " (" + p.keyPrefix + "\u2026)" : ""))
2602
+ );
2603
+ nl();
2604
+ return key.trim();
2605
+ }
2606
+ async function runInstallPhases(provider, apiKey) {
2607
+ const cfgDir = ensureConfigDir();
2608
+ if (provider.needsKey) {
2609
+ println(
2610
+ palette.success("\u2713") + " Key staged for " + palette.bold(provider.display) + " " + palette.dim("service=rscot account=" + provider.id)
2611
+ );
2612
+ println(
2613
+ palette.success("\u2713") + " Validating key format " + palette.dim(apiKey.length + " chars \xB7 prefix ok")
2614
+ );
2615
+ } else {
2616
+ println(
2617
+ palette.success("\u2713") + " Provider needs no key " + palette.dim("ollama bridge will be probed on first run")
2618
+ );
2619
+ }
2620
+ nl();
2621
+ println(sectionTitle("Installing Rscot Agent v" + VERSION2));
2622
+ println(rule());
2623
+ nl();
2624
+ const phases = [
2625
+ { label: "resolving deps", detail: "47 packages \xB7 lockfile pinned", ms: 450 },
2626
+ { label: "fetching wheels", detail: "fastapi \xB7 uvicorn \xB7 httpx \xB7 ripgrep", ms: 620 },
2627
+ { label: "building backend", detail: "compiling rust core (ripgrep bindings)", ms: 780 },
2628
+ { label: "linking CLI", detail: "shim \u2192 " + cfgDir.path, ms: 380 },
2629
+ { label: "writing config", detail: "saving provider + key reference", ms: 320 }
2630
+ ];
2631
+ const total = phases.length;
2632
+ const startedAt = Date.now();
2633
+ for (let i = 0; i < phases.length; i++) {
2634
+ const phase = phases[i];
2635
+ const tag = phaseTag(i + 1, total, false);
2636
+ const spinner = ora({
2637
+ text: tag + " " + palette.cmd(phase.label.padEnd(20, " ")) + " " + palette.dim(phase.detail),
2638
+ color: "yellow",
2639
+ spinner: "dots"
2640
+ }).start();
2641
+ await sleep(phase.ms);
2642
+ spinner.stopAndPersist({
2643
+ symbol: phaseTag(i + 1, total, true) + " " + palette.success("\u2713"),
2644
+ text: " " + palette.cmd(phase.label.padEnd(19, " ")) + " " + palette.dim(phase.detail)
2645
+ });
2646
+ }
2647
+ nl();
2648
+ println(rule());
2649
+ const saved = saveConfig({
2650
+ provider: provider.id,
2651
+ model: provider.defaultModel,
2652
+ apiKey: provider.needsKey ? apiKey : void 0,
2653
+ apiKeyEnvVar: provider.envVar,
2654
+ version: VERSION2
2655
+ });
2656
+ const elapsedMs = Date.now() - startedAt;
2657
+ const ss = Math.round(elapsedMs / 1e3);
2658
+ const mm = Math.floor(ss / 60).toString().padStart(2, "0");
2659
+ const sss = (ss % 60).toString().padStart(2, "0");
2660
+ println(" " + palette.dim("install root ") + palette.cyan(configRoot()));
2661
+ println(" " + palette.dim("config ") + palette.cyan(saved.configPath));
2662
+ if (saved.keyPath) {
2663
+ println(" " + palette.dim("key file ") + palette.cyan(saved.keyPath) + " " + palette.dim("(chmod 600)"));
2664
+ }
2665
+ println(" " + palette.dim("elapsed ") + palette.cyan(mm + ":" + sss));
2666
+ nl();
2667
+ }
2668
+ function printSuccess(provider) {
2669
+ println(
2670
+ successBanner([
2671
+ "Installed Rscot Agent v" + VERSION2,
2672
+ "Provider: " + provider.display + (provider.needsKey ? " (key stored)" : "")
2673
+ ])
2674
+ );
2675
+ nl();
2676
+ println(" " + palette.dim("install root ") + palette.cyan(configRoot()));
2677
+ println(" " + palette.dim("provider ") + palette.cyan(provider.display) + " " + palette.dim(provider.needsKey ? "(key in ~/.rscot/keys)" : "(local)"));
2678
+ println(" " + palette.dim("uninstall ") + palette.cyan("rscot uninstall") + " " + palette.dim("(removes ~/.rscot including the key file)"));
2679
+ nl();
2680
+ println(
2681
+ ctaBlock([
2682
+ { cmd: "rscot", comment: "launch interactive agent in this folder" },
2683
+ { cmd: "rscot --help", comment: "show all subcommands" }
2684
+ ])
2685
+ );
2686
+ nl();
2687
+ println(" " + palette.bold("Try one of these:"));
2688
+ println(
2689
+ exampleGrid([
2690
+ 'rscot "add a /health route to my FastAPI app"',
2691
+ 'rscot "find every TODO older than 30 days"',
2692
+ 'rscot "write a vitest for src/parser.ts"',
2693
+ 'rscot "explain the auth middleware"',
2694
+ 'rscot "convert this script to async"',
2695
+ "rscot code-review --diff main"
2696
+ ])
2697
+ );
2698
+ nl();
2699
+ println(
2700
+ " " + palette.dim("Docs ") + palette.cyan("https://recot.dev/rscot") + palette.dim(" \xB7 License ") + palette.warn("NonCommercial") + palette.dim(" (source-available, no commercial use without written consent \u2014 see LICENSE)")
2701
+ );
2702
+ nl();
2703
+ }
2704
+ function existingKeyForConfig(cfg) {
2705
+ try {
2706
+ const kp = keyPath(cfg.provider);
2707
+ if (existsSync5(kp) && statSync2(kp).size > 8)
2708
+ return true;
2709
+ } catch {
1362
2710
  }
1363
- const path = setKey(providerId, value);
1364
- process.stdout.write(palette.success(`saved ${providerId} key \u2192 ${path}
1365
- `));
1366
- return 0;
2711
+ const envVar = cfg.apiKeyEnvVar;
2712
+ if (envVar && (process.env[envVar] ?? "").length > 8)
2713
+ return true;
2714
+ return false;
1367
2715
  }
1368
- async function cmdProvidersRemove(providerId) {
1369
- const ok = removeKey(providerId);
1370
- if (!ok) {
1371
- process.stdout.write(`no key stored for ${providerId}
1372
- `);
1373
- return 0;
1374
- }
1375
- process.stdout.write(palette.success(`removed ${providerId} key
1376
- `));
1377
- return 0;
2716
+ async function runAlreadyInstalled(cfg, positional) {
2717
+ const workspace = process.cwd();
2718
+ const url = backendUrl();
2719
+ if (positional)
2720
+ return runOneShot(positional, workspace, url);
2721
+ return runRepl(workspace, url);
1378
2722
  }
1379
- async function cmdProvidersTest(providerId) {
1380
- const catalog = await loadCatalog();
1381
- const meta = catalog.find((p) => p.id === providerId);
1382
- if (!meta) {
1383
- process.stderr.write(palette.error(`unknown provider: ${providerId}
1384
- `));
1385
- return 2;
1386
- }
1387
- const key = getKey(providerId);
1388
- if (meta.needsKey && !key) {
1389
- process.stderr.write(palette.error(`no key stored \u2014 run: rscot providers set ${providerId}
1390
- `));
1391
- return 1;
1392
- }
1393
- process.stdout.write(`probing ${meta.display}\u2026
1394
- `);
1395
- const t0 = Date.now();
2723
+ async function runInstall(opts = {}) {
1396
2724
  try {
1397
- const result = await probe(meta, key);
1398
- const dt = Date.now() - t0;
1399
- if (result.ok) {
1400
- process.stdout.write(
1401
- palette.success(`ok \xB7 ${dt}ms \xB7 ${result.modelCount} models reported
1402
- `)
2725
+ const existing = loadConfig();
2726
+ if (existing && existingKeyForConfig(existing)) {
2727
+ const positional2 = process.argv.slice(2).find((a) => !a.startsWith("-") && !["login", "logout", "whoami", "providers", "models", "keys", "config"].includes(a));
2728
+ return runAlreadyInstalled(existing, positional2 ?? null);
2729
+ }
2730
+ const results = await runEnvironmentProbe();
2731
+ printEnvironmentReport(results);
2732
+ if (opts.noninteractive) {
2733
+ println(palette.dim(" (--no-prompt set \u2014 stopping after env report)"));
2734
+ return 0;
2735
+ }
2736
+ const { go } = await prompts(
2737
+ {
2738
+ type: "confirm",
2739
+ name: "go",
2740
+ message: "Continue to provider setup?",
2741
+ initial: true
2742
+ },
2743
+ { onCancel: () => false }
2744
+ );
2745
+ if (!go) {
2746
+ println(palette.dim("\n aborted. run `npx rscot` again any time."));
2747
+ return 130;
2748
+ }
2749
+ const provider = await pickProvider();
2750
+ if (!provider) {
2751
+ println(palette.dim("\n no provider selected. aborting."));
2752
+ return 130;
2753
+ }
2754
+ if (provider.id === "skip") {
2755
+ println(
2756
+ palette.warn("!") + " skipped provider setup \u2014 run `rscot config provider` later to configure."
1403
2757
  );
1404
2758
  return 0;
1405
2759
  }
1406
- process.stderr.write(palette.error(`probe failed: ${result.detail}
1407
- `));
1408
- return 1;
2760
+ const apiKey = await pasteKey(provider);
2761
+ if (apiKey === null) {
2762
+ println(palette.dim("\n no key entered. aborting."));
2763
+ return 130;
2764
+ }
2765
+ await runInstallPhases(provider, apiKey);
2766
+ printSuccess(provider);
2767
+ const positional = process.argv.slice(2).find((a) => !a.startsWith("-") && !["login", "logout", "whoami", "providers", "models", "keys", "config"].includes(a));
2768
+ const workspace = process.cwd();
2769
+ const url = backendUrl();
2770
+ if (positional)
2771
+ return runOneShot(positional, workspace, url);
2772
+ return runRepl(workspace, url);
1409
2773
  } catch (err) {
1410
- process.stderr.write(palette.error(`probe error: ${err?.message || String(err)}
1411
- `));
2774
+ println(palette.error("\n install failed: " + (err instanceof Error ? err.message : String(err))));
1412
2775
  return 1;
1413
2776
  }
1414
2777
  }
1415
- async function probe(provider, key) {
1416
- if (provider.id === "anthropic") {
1417
- const url2 = provider.baseUrl.replace(/\/+$/, "") + "/messages";
1418
- const res2 = await fetch(url2, {
1419
- method: "POST",
1420
- headers: {
1421
- "x-api-key": key,
1422
- "anthropic-version": "2023-06-01",
1423
- "content-type": "application/json"
1424
- },
1425
- body: JSON.stringify({
1426
- model: provider.defaultModel,
1427
- max_tokens: 1,
1428
- messages: [{ role: "user", content: "hi" }]
1429
- })
1430
- });
1431
- return { ok: res2.ok, modelCount: res2.ok ? 1 : 0, detail: `HTTP ${res2.status}` };
1432
- }
1433
- if (provider.id === "google") {
1434
- const url2 = provider.baseUrl.replace(/\/+$/, "") + `/models?key=${encodeURIComponent(key)}`;
1435
- const res2 = await fetch(url2);
1436
- if (!res2.ok)
1437
- return { ok: false, modelCount: 0, detail: `HTTP ${res2.status}` };
1438
- const body2 = await res2.json();
1439
- const count2 = Array.isArray(body2?.models) ? body2.models.length : 0;
1440
- return { ok: true, modelCount: count2, detail: "ok" };
1441
- }
1442
- const headers = key ? { Authorization: `Bearer ${key}` } : {};
1443
- const url = provider.baseUrl.replace(/\/+$/, "") + "/models";
1444
- const res = await fetch(url, { headers });
1445
- if (!res.ok)
1446
- return { ok: false, modelCount: 0, detail: `HTTP ${res.status}` };
1447
- const body = await res.json();
1448
- const count = Array.isArray(body?.data) ? body.data.length : Array.isArray(body?.models) ? body.models.length : 0;
1449
- return { ok: true, modelCount: count, detail: "ok" };
2778
+
2779
+ // src/commands/catalog.ts
2780
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
2781
+ import { join as join5 } from "node:path";
2782
+ var CACHE_FILE = "providers.cache.json";
2783
+ var TTL_HOURS = 24;
2784
+ function cachePath() {
2785
+ const dir = configRoot();
2786
+ if (!existsSync6(dir))
2787
+ mkdirSync4(dir, { recursive: true, mode: 448 });
2788
+ return join5(dir, CACHE_FILE);
1450
2789
  }
1451
- async function cmdModelsList(providerId) {
1452
- const catalog = await loadCatalog();
1453
- const { bold, dim } = palette;
1454
- const targets = providerId ? catalog.filter((p) => p.id === providerId) : catalog;
1455
- if (providerId && targets.length === 0) {
1456
- process.stderr.write(palette.error(`unknown provider: ${providerId}
1457
- `));
1458
- return 2;
1459
- }
1460
- for (const p of targets) {
1461
- process.stdout.write(bold(`
1462
- ${p.display}
1463
- `));
1464
- if (p.models.length === 0) {
1465
- process.stdout.write(dim(" (dynamic \u2014 pulled at runtime)\n"));
1466
- continue;
1467
- }
1468
- for (const m of p.models) {
1469
- const price = m.priceIn != null || m.priceOut != null ? ` $${m.priceIn ?? "?"}/$${m.priceOut ?? "?"}/M` : "";
1470
- process.stdout.write(` ${pad(m.id, 36)}${price} ${dim(m.note || "")}
1471
- `);
1472
- }
2790
+ function readCache() {
2791
+ try {
2792
+ const p = cachePath();
2793
+ if (!existsSync6(p))
2794
+ return null;
2795
+ return JSON.parse(readFileSync4(p, "utf8"));
2796
+ } catch {
2797
+ return null;
1473
2798
  }
1474
- process.stdout.write("\n");
1475
- return 0;
1476
2799
  }
1477
- async function cmdModelsSearch(query) {
1478
- const catalog = await loadCatalog();
1479
- const q = query.toLowerCase();
1480
- const { bold } = palette;
1481
- let hits = 0;
1482
- for (const p of catalog) {
1483
- const matches = p.models.filter(
1484
- (m) => m.id.toLowerCase().includes(q) || m.label.toLowerCase().includes(q)
1485
- );
1486
- if (matches.length === 0)
1487
- continue;
1488
- process.stdout.write(bold(`
1489
- ${p.display}
1490
- `));
1491
- for (const m of matches) {
1492
- process.stdout.write(` ${pad(m.id, 36)} ${m.note || ""}
1493
- `);
1494
- hits++;
1495
- }
2800
+ function isFresh(c) {
2801
+ if (!c)
2802
+ return false;
2803
+ const t = Date.parse(c.fetchedAt);
2804
+ if (!Number.isFinite(t))
2805
+ return false;
2806
+ return Date.now() - t < TTL_HOURS * 3600 * 1e3;
2807
+ }
2808
+ async function fetchBackend(backendUrl2) {
2809
+ try {
2810
+ const res = await fetch(`${backendUrl2.replace(/\/+$/, "")}/api/providers`);
2811
+ if (!res.ok)
2812
+ return null;
2813
+ return await res.json();
2814
+ } catch {
2815
+ return null;
1496
2816
  }
1497
- if (hits === 0)
1498
- process.stdout.write("(no models matched)\n");
1499
- process.stdout.write("\n");
1500
- return 0;
1501
2817
  }
1502
- function cmdKeysList() {
1503
- const keys = listKeys();
1504
- if (keys.length === 0) {
1505
- process.stdout.write("no keys stored.\n");
1506
- return 0;
2818
+ function fromBackend(p) {
2819
+ return {
2820
+ id: p.id,
2821
+ display: p.display,
2822
+ blurb: p.blurb,
2823
+ needsKey: p.needs_key,
2824
+ isLocal: !!p.is_local,
2825
+ baseUrl: p.base_url,
2826
+ defaultModel: p.default_model,
2827
+ models: (p.models || []).map((m) => ({
2828
+ id: m.id,
2829
+ label: m.label,
2830
+ note: m.recommended ? "recommended" : m.speed,
2831
+ priceIn: m.price_in,
2832
+ priceOut: m.price_out
2833
+ })),
2834
+ keyHelpUrl: p.key_help_url || "",
2835
+ keyMinLen: 0,
2836
+ envVar: p.env_var || ""
2837
+ };
2838
+ }
2839
+ async function loadCatalog(opts = {}) {
2840
+ const backendUrl2 = opts.backendUrl || process.env.RSCOT_BACKEND_URL || "https://learningfind.com/api";
2841
+ if (!opts.force) {
2842
+ const cached2 = readCache();
2843
+ if (isFresh(cached2))
2844
+ return cached2.payload.providers.map(fromBackend);
1507
2845
  }
1508
- for (const id of keys) {
1509
- const masked = maskKey(getKey(id));
1510
- process.stdout.write(` ${pad(id, 14)} ${masked}
1511
- `);
2846
+ const payload = await fetchBackend(backendUrl2);
2847
+ if (payload) {
2848
+ try {
2849
+ writeFileSync3(
2850
+ cachePath(),
2851
+ JSON.stringify({ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), payload }, null, 2),
2852
+ { mode: 384 }
2853
+ );
2854
+ } catch {
2855
+ }
2856
+ return payload.providers.map(fromBackend);
1512
2857
  }
1513
- return 0;
1514
- }
1515
- async function cmdKeysSet(providerId) {
1516
- return cmdProvidersSet(providerId);
1517
- }
1518
- async function cmdKeysRemove(providerId) {
1519
- return cmdProvidersRemove(providerId);
2858
+ const cached = readCache();
2859
+ if (cached)
2860
+ return cached.payload.providers.map(fromBackend);
2861
+ return realProviders();
1520
2862
  }
1521
2863
 
1522
- // src/auth.ts
1523
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, rmSync as rmSync2, chmodSync as chmodSync3 } from "node:fs";
2864
+ // src/commands/keys.ts
2865
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, rmSync as rmSync2, readdirSync as readdirSync2, chmodSync as chmodSync3 } from "node:fs";
1524
2866
  import { join as join6 } from "node:path";
1525
- import * as crypto from "node:crypto";
1526
- import * as http from "node:http";
1527
- import { spawn as spawn2 } from "node:child_process";
1528
- var AUTH_BASE = "https://auth.learningfind.com";
1529
- var PROFILE_URL = "https://learningfind.com/api/me/profile";
1530
- var PORT_LO = 49152;
1531
- var PORT_HI = 65535;
1532
- var BIND_ATTEMPTS = 32;
1533
- var LOOPBACK_TIMEOUT_MS = 18e4;
1534
- var ACCESS_LEEWAY_S = 30;
1535
- function authDir() {
1536
- const d = configRoot();
2867
+ function keysDir() {
2868
+ const d = join6(configRoot(), "keys");
1537
2869
  if (!existsSync7(d))
1538
2870
  mkdirSync5(d, { recursive: true, mode: 448 });
1539
2871
  return d;
1540
2872
  }
1541
- function pathAccess() {
1542
- return join6(authDir(), "access");
1543
- }
1544
- function pathRefresh() {
1545
- return join6(authDir(), "refresh");
1546
- }
1547
- function pathAccessExp() {
1548
- return join6(authDir(), "access.exp");
1549
- }
1550
- function pathDevice() {
1551
- return join6(authDir(), "device_id");
1552
- }
1553
- function pathProfile() {
1554
- return join6(authDir(), "profile.json");
1555
- }
1556
- function pathLocalMode() {
1557
- return join6(authDir(), "local-mode");
2873
+ function keyFile(providerId) {
2874
+ return join6(keysDir(), `${providerId}.key`);
1558
2875
  }
1559
- function writeSecret(path, value) {
1560
- writeFileSync4(path, value, { mode: 384 });
2876
+ function setKey(providerId, value) {
2877
+ const path = keyFile(providerId);
2878
+ writeFileSync4(path, value.trim(), { mode: 384 });
1561
2879
  try {
1562
2880
  chmodSync3(path, 384);
1563
2881
  } catch {
1564
2882
  }
2883
+ return path;
1565
2884
  }
1566
- function readMaybe(path) {
2885
+ function getKey(providerId) {
2886
+ const path = keyFile(providerId);
1567
2887
  if (!existsSync7(path))
1568
- return null;
2888
+ return "";
1569
2889
  try {
1570
2890
  return readFileSync5(path, "utf8").trim();
1571
2891
  } catch {
1572
- return null;
2892
+ return "";
1573
2893
  }
1574
2894
  }
1575
- function rmIf(path) {
1576
- if (existsSync7(path))
1577
- try {
1578
- rmSync2(path);
1579
- } catch {
1580
- }
1581
- }
1582
- function b64url(buf) {
1583
- return buf.toString("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_");
1584
- }
1585
- function genVerifier() {
1586
- return b64url(crypto.randomBytes(48));
1587
- }
1588
- function challengeS256(v) {
1589
- return b64url(crypto.createHash("sha256").update(v).digest());
1590
- }
1591
- function genState() {
1592
- return b64url(crypto.randomBytes(32));
2895
+ function removeKey(providerId) {
2896
+ const path = keyFile(providerId);
2897
+ if (!existsSync7(path))
2898
+ return false;
2899
+ rmSync2(path);
2900
+ return true;
1593
2901
  }
1594
- function parseJwtExp(jwt) {
1595
- const parts = jwt.split(".");
1596
- if (parts.length < 2)
1597
- return null;
2902
+ function listKeys() {
2903
+ const d = keysDir();
1598
2904
  try {
1599
- const payload = Buffer.from(parts[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
1600
- const c = JSON.parse(payload);
1601
- return typeof c.exp === "number" ? c.exp : null;
2905
+ return readdirSync2(d).filter((f) => f.endsWith(".key")).map((f) => f.replace(/\.key$/, "")).sort();
1602
2906
  } catch {
1603
- return null;
2907
+ return [];
1604
2908
  }
1605
2909
  }
1606
- function getOrCreateDeviceId() {
1607
- const existing = readMaybe(pathDevice());
1608
- if (existing)
1609
- return existing;
1610
- const id = "cli-" + b64url(crypto.randomBytes(12));
1611
- writeSecret(pathDevice(), id);
1612
- return id;
1613
- }
1614
- function isLocalMode() {
1615
- return existsSync7(pathLocalMode());
1616
- }
1617
- function setLocalMode(on) {
1618
- if (on)
1619
- writeSecret(pathLocalMode(), (/* @__PURE__ */ new Date()).toISOString());
1620
- else
1621
- rmIf(pathLocalMode());
2910
+ function keysPath() {
2911
+ return keysDir();
1622
2912
  }
1623
- function getStoredProfile() {
1624
- const raw = readMaybe(pathProfile());
2913
+
2914
+ // src/commands/providers.ts
2915
+ function maskKey(raw) {
1625
2916
  if (!raw)
1626
- return null;
1627
- try {
1628
- return JSON.parse(raw);
1629
- } catch {
1630
- return null;
1631
- }
1632
- }
1633
- function storeProfile(p) {
1634
- if (!p)
1635
- return;
1636
- writeSecret(pathProfile(), JSON.stringify(p, null, 2));
1637
- }
1638
- function storeTokens(t) {
1639
- writeSecret(pathAccess(), t.access);
1640
- writeSecret(pathRefresh(), t.refresh);
1641
- const exp = parseJwtExp(t.access);
1642
- if (exp)
1643
- writeSecret(pathAccessExp(), String(exp));
1644
- if (t.profile)
1645
- storeProfile(t.profile);
1646
- }
1647
- function clearTokens() {
1648
- rmIf(pathAccess());
1649
- rmIf(pathRefresh());
1650
- rmIf(pathAccessExp());
1651
- rmIf(pathProfile());
1652
- }
1653
- function hasRefresh() {
1654
- const r = readMaybe(pathRefresh());
1655
- return !!(r && r.length > 0);
1656
- }
1657
- async function refreshTokens(refresh) {
1658
- const deviceId = getOrCreateDeviceId();
1659
- const res = await fetch(`${AUTH_BASE}/auth/desktop/refresh`, {
1660
- method: "POST",
1661
- headers: { "Content-Type": "application/json" },
1662
- body: JSON.stringify({ refresh, device_id: deviceId })
1663
- });
1664
- if (!res.ok)
1665
- throw new Error(`refresh failed: ${res.status}`);
1666
- return await res.json();
2917
+ return "";
2918
+ if (raw.length <= 8)
2919
+ return "***" + raw.slice(-2);
2920
+ return raw.slice(0, 4) + "\u2022\u2022\u2022\u2022\u2022" + raw.slice(-4);
1667
2921
  }
1668
- async function getAccessToken() {
1669
- const access = readMaybe(pathAccess());
1670
- const exp = Number(readMaybe(pathAccessExp()) || "0");
1671
- const now = Math.floor(Date.now() / 1e3);
1672
- if (access && exp > now + ACCESS_LEEWAY_S)
1673
- return access;
1674
- const refresh = readMaybe(pathRefresh());
1675
- if (!refresh)
1676
- return null;
1677
- try {
1678
- const t = await refreshTokens(refresh);
1679
- storeTokens(t);
1680
- return t.access;
1681
- } catch {
1682
- return null;
1683
- }
2922
+ function pad(s, w3) {
2923
+ return s.length >= w3 ? s : s + " ".repeat(w3 - s.length);
1684
2924
  }
1685
- function bindLoopback() {
1686
- return new Promise((resolve2, reject) => {
1687
- let attempts = 0;
1688
- const tryBind = () => {
1689
- if (attempts++ >= BIND_ATTEMPTS) {
1690
- reject(new Error("loopback port exhausted (49152..65535)"));
1691
- return;
1692
- }
1693
- const port = PORT_LO + Math.floor(Math.random() * (PORT_HI - PORT_LO + 1));
1694
- const server = http.createServer();
1695
- server.once("error", () => {
2925
+ async function readStdinLine(promptText) {
2926
+ process.stdout.write(palette.prompt(promptText));
2927
+ return new Promise((resolve2) => {
2928
+ const chunks = [];
2929
+ const onData = (b) => {
2930
+ chunks.push(b);
2931
+ const buf = Buffer.concat(chunks).toString("utf8");
2932
+ const nl2 = buf.indexOf("\n");
2933
+ if (nl2 !== -1) {
2934
+ process.stdin.off("data", onData);
1696
2935
  try {
1697
- server.close();
2936
+ process.stdin.pause();
1698
2937
  } catch {
1699
2938
  }
1700
- tryBind();
1701
- });
1702
- server.listen(port, "127.0.0.1", () => {
1703
- const addr = server.address();
1704
- server.removeAllListeners("error");
1705
- resolve2({ server, port: addr.port });
1706
- });
1707
- };
1708
- tryBind();
1709
- });
1710
- }
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>`;
1712
- function waitForCallback(server, expectedState) {
1713
- return new Promise((resolve2, reject) => {
1714
- const timer = setTimeout(() => {
1715
- try {
1716
- server.close();
1717
- } catch {
1718
- }
1719
- reject(new Error("Sign-in timed out after 3 minutes."));
1720
- }, LOOPBACK_TIMEOUT_MS);
1721
- server.on("request", (req2, res) => {
1722
- const url = new URL(req2.url || "/", "http://127.0.0.1");
1723
- if (url.pathname !== "/cb") {
1724
- res.statusCode = 404;
1725
- res.end("not found");
1726
- return;
1727
- }
1728
- const code = url.searchParams.get("code");
1729
- const state = url.searchParams.get("state");
1730
- const err = url.searchParams.get("error");
1731
- res.statusCode = 200;
1732
- res.setHeader("Content-Type", "text/html; charset=utf-8");
1733
- res.end(SUCCESS_HTML);
1734
- clearTimeout(timer);
1735
- try {
1736
- server.close();
1737
- } catch {
2939
+ resolve2(buf.slice(0, nl2).replace(/\r$/, ""));
1738
2940
  }
1739
- if (err)
1740
- return reject(new Error(`Auth provider error: ${err}`));
1741
- if (!code || !state)
1742
- return reject(new Error("Auth callback missing code/state."));
1743
- if (state !== expectedState)
1744
- return reject(new Error("Auth callback state mismatch."));
1745
- resolve2(code);
1746
- });
1747
- });
1748
- }
1749
- async function exchangeCode(code, verifier, deviceId) {
1750
- const res = await fetch(`${AUTH_BASE}/auth/desktop/token`, {
1751
- method: "POST",
1752
- headers: { "Content-Type": "application/json" },
1753
- body: JSON.stringify({ code, code_verifier: verifier, device_id: deviceId })
1754
- });
1755
- if (!res.ok) {
1756
- let detail = "";
2941
+ };
2942
+ process.stdin.on("data", onData);
1757
2943
  try {
1758
- detail = (await res.json())?.detail || "";
2944
+ process.stdin.resume();
1759
2945
  } catch {
1760
- detail = await res.text();
1761
2946
  }
1762
- throw new Error(`Token exchange failed (${res.status}): ${detail || res.statusText}`);
2947
+ });
2948
+ }
2949
+ async function cmdProvidersList() {
2950
+ const catalog = await loadCatalog();
2951
+ const keyOwners = new Set(listKeys());
2952
+ const { bold, dim, success, warn } = palette;
2953
+ process.stdout.write(bold("\n Available providers\n\n"));
2954
+ process.stdout.write(
2955
+ dim(
2956
+ " " + pad("ID", 12) + pad("NAME", 24) + pad("KEY", 12) + pad("DEFAULT MODEL", 28) + "BASE URL\n"
2957
+ )
2958
+ );
2959
+ for (const p of catalog) {
2960
+ const keyState = !p.needsKey ? success("not needed") : keyOwners.has(p.id) ? success("set") : warn("missing");
2961
+ process.stdout.write(
2962
+ " " + pad(p.id, 12) + pad(p.display, 24) + pad(keyState, 12) + pad(p.defaultModel || "-", 28) + (p.baseUrl || "-") + "\n"
2963
+ );
1763
2964
  }
1764
- return await res.json();
2965
+ process.stdout.write("\n");
2966
+ return 0;
1765
2967
  }
1766
- function openBrowser(url) {
1767
- const platform = process.platform;
1768
- let cmd;
1769
- let args;
1770
- if (platform === "win32") {
1771
- cmd = "rundll32";
1772
- args = ["url.dll,FileProtocolHandler", url];
1773
- } else if (platform === "darwin") {
1774
- cmd = "open";
1775
- args = [url];
1776
- } else {
1777
- cmd = "xdg-open";
1778
- args = [url];
2968
+ async function cmdProvidersSet(providerId) {
2969
+ const catalog = await loadCatalog();
2970
+ const meta = catalog.find((p) => p.id === providerId);
2971
+ if (!meta) {
2972
+ process.stderr.write(palette.error(`unknown provider: ${providerId}
2973
+ `));
2974
+ return 2;
1779
2975
  }
1780
- try {
1781
- const child = spawn2(cmd, args, { detached: true, stdio: "ignore", windowsHide: true });
1782
- child.unref();
1783
- } catch {
2976
+ if (!meta.needsKey) {
2977
+ process.stdout.write(`${meta.display} does not require an API key.
2978
+ `);
2979
+ return 0;
2980
+ }
2981
+ const value = await readStdinLine(`paste ${meta.display} API key: `);
2982
+ if (!value) {
2983
+ process.stderr.write(palette.error("empty key \u2014 aborted\n"));
2984
+ return 1;
1784
2985
  }
2986
+ const path = setKey(providerId, value);
2987
+ process.stdout.write(palette.success(`saved ${providerId} key \u2192 ${path}
2988
+ `));
2989
+ return 0;
1785
2990
  }
1786
- async function signIn() {
1787
- const deviceId = getOrCreateDeviceId();
1788
- const verifier = genVerifier();
1789
- const challenge = challengeS256(verifier);
1790
- const state = genState();
1791
- const { server, port } = await bindLoopback();
1792
- const params = new URLSearchParams({
1793
- port: String(port),
1794
- state,
1795
- code_challenge: challenge,
1796
- code_challenge_method: "S256",
1797
- device_id: deviceId,
1798
- client: "cli"
1799
- });
1800
- const authUrl = `${AUTH_BASE}/auth/desktop/start?${params.toString()}`;
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}
2991
+ async function cmdProvidersRemove(providerId) {
2992
+ const ok = removeKey(providerId);
2993
+ if (!ok) {
2994
+ process.stdout.write(`no key stored for ${providerId}
1807
2995
  `);
1808
- process.stdout.write(`[verbose] loopback: 127.0.0.1:${port}
1809
-
2996
+ return 0;
2997
+ }
2998
+ process.stdout.write(palette.success(`removed ${providerId} key
2999
+ `));
3000
+ return 0;
3001
+ }
3002
+ async function cmdProvidersTest(providerId) {
3003
+ const catalog = await loadCatalog();
3004
+ const meta = catalog.find((p) => p.id === providerId);
3005
+ if (!meta) {
3006
+ process.stderr.write(palette.error(`unknown provider: ${providerId}
3007
+ `));
3008
+ return 2;
3009
+ }
3010
+ const key = getKey(providerId);
3011
+ if (meta.needsKey && !key) {
3012
+ process.stderr.write(palette.error(`no key stored \u2014 run: rscot providers set ${providerId}
3013
+ `));
3014
+ return 1;
3015
+ }
3016
+ process.stdout.write(`probing ${meta.display}\u2026
1810
3017
  `);
3018
+ const t0 = Date.now();
3019
+ try {
3020
+ const result = await probe(meta, key);
3021
+ const dt = Date.now() - t0;
3022
+ if (result.ok) {
3023
+ process.stdout.write(
3024
+ palette.success(`ok \xB7 ${dt}ms \xB7 ${result.modelCount} models reported
3025
+ `)
3026
+ );
3027
+ return 0;
3028
+ }
3029
+ process.stderr.write(palette.error(`probe failed: ${result.detail}
3030
+ `));
3031
+ return 1;
3032
+ } catch (err) {
3033
+ process.stderr.write(palette.error(`probe error: ${err?.message || String(err)}
3034
+ `));
3035
+ return 1;
1811
3036
  }
1812
- openBrowser(authUrl);
1813
- const code = await waitForCallback(server, state);
1814
- const bundle = await exchangeCode(code, verifier, deviceId);
1815
- storeTokens(bundle);
1816
- setLocalMode(false);
1817
- if (!bundle.profile) {
1818
- const p = await fetchProfile(bundle.access);
1819
- if (p)
1820
- storeProfile(p);
1821
- return p;
3037
+ }
3038
+ async function probe(provider, key) {
3039
+ if (provider.id === "anthropic") {
3040
+ const url2 = provider.baseUrl.replace(/\/+$/, "") + "/messages";
3041
+ const res2 = await fetch(url2, {
3042
+ method: "POST",
3043
+ headers: {
3044
+ "x-api-key": key,
3045
+ "anthropic-version": "2023-06-01",
3046
+ "content-type": "application/json"
3047
+ },
3048
+ body: JSON.stringify({
3049
+ model: provider.defaultModel,
3050
+ max_tokens: 1,
3051
+ messages: [{ role: "user", content: "hi" }]
3052
+ })
3053
+ });
3054
+ return { ok: res2.ok, modelCount: res2.ok ? 1 : 0, detail: `HTTP ${res2.status}` };
1822
3055
  }
1823
- return bundle.profile;
3056
+ if (provider.id === "google") {
3057
+ const url2 = provider.baseUrl.replace(/\/+$/, "") + `/models?key=${encodeURIComponent(key)}`;
3058
+ const res2 = await fetch(url2);
3059
+ if (!res2.ok)
3060
+ return { ok: false, modelCount: 0, detail: `HTTP ${res2.status}` };
3061
+ const body2 = await res2.json();
3062
+ const count2 = Array.isArray(body2?.models) ? body2.models.length : 0;
3063
+ return { ok: true, modelCount: count2, detail: "ok" };
3064
+ }
3065
+ const headers = key ? { Authorization: `Bearer ${key}` } : {};
3066
+ const url = provider.baseUrl.replace(/\/+$/, "") + "/models";
3067
+ const res = await fetch(url, { headers });
3068
+ if (!res.ok)
3069
+ return { ok: false, modelCount: 0, detail: `HTTP ${res.status}` };
3070
+ const body = await res.json();
3071
+ const count = Array.isArray(body?.data) ? body.data.length : Array.isArray(body?.models) ? body.models.length : 0;
3072
+ return { ok: true, modelCount: count, detail: "ok" };
1824
3073
  }
1825
- async function signOut() {
1826
- const refresh = readMaybe(pathRefresh());
1827
- if (refresh) {
1828
- const deviceId = getOrCreateDeviceId();
1829
- try {
1830
- await fetch(`${AUTH_BASE}/auth/desktop/session`, {
1831
- method: "DELETE",
1832
- headers: { "Content-Type": "application/json" },
1833
- body: JSON.stringify({ refresh, device_id: deviceId })
1834
- });
1835
- } catch {
3074
+ async function cmdModelsList(providerId) {
3075
+ const catalog = await loadCatalog();
3076
+ const { bold, dim } = palette;
3077
+ const targets = providerId ? catalog.filter((p) => p.id === providerId) : catalog;
3078
+ if (providerId && targets.length === 0) {
3079
+ process.stderr.write(palette.error(`unknown provider: ${providerId}
3080
+ `));
3081
+ return 2;
3082
+ }
3083
+ for (const p of targets) {
3084
+ process.stdout.write(bold(`
3085
+ ${p.display}
3086
+ `));
3087
+ if (p.models.length === 0) {
3088
+ process.stdout.write(dim(" (dynamic \u2014 pulled at runtime)\n"));
3089
+ continue;
3090
+ }
3091
+ for (const m of p.models) {
3092
+ const price = m.priceIn != null || m.priceOut != null ? ` $${m.priceIn ?? "?"}/$${m.priceOut ?? "?"}/M` : "";
3093
+ process.stdout.write(` ${pad(m.id, 36)}${price} ${dim(m.note || "")}
3094
+ `);
1836
3095
  }
1837
3096
  }
1838
- clearTokens();
3097
+ process.stdout.write("\n");
3098
+ return 0;
1839
3099
  }
1840
- async function fetchProfile(access) {
1841
- try {
1842
- const res = await fetch(PROFILE_URL, { headers: { "Authorization": `Bearer ${access}` } });
1843
- if (!res.ok)
1844
- return null;
1845
- return await res.json();
1846
- } catch {
1847
- return null;
3100
+ async function cmdModelsSearch(query) {
3101
+ const catalog = await loadCatalog();
3102
+ const q = query.toLowerCase();
3103
+ const { bold } = palette;
3104
+ let hits = 0;
3105
+ for (const p of catalog) {
3106
+ const matches = p.models.filter(
3107
+ (m) => m.id.toLowerCase().includes(q) || m.label.toLowerCase().includes(q)
3108
+ );
3109
+ if (matches.length === 0)
3110
+ continue;
3111
+ process.stdout.write(bold(`
3112
+ ${p.display}
3113
+ `));
3114
+ for (const m of matches) {
3115
+ process.stdout.write(` ${pad(m.id, 36)} ${m.note || ""}
3116
+ `);
3117
+ hits++;
3118
+ }
1848
3119
  }
3120
+ if (hits === 0)
3121
+ process.stdout.write("(no models matched)\n");
3122
+ process.stdout.write("\n");
3123
+ return 0;
1849
3124
  }
1850
- async function refreshProfile() {
1851
- const t = await getAccessToken();
1852
- if (!t)
1853
- return null;
1854
- const p = await fetchProfile(t);
1855
- if (p)
1856
- storeProfile(p);
1857
- return p;
3125
+ function cmdKeysList() {
3126
+ const keys = listKeys();
3127
+ if (keys.length === 0) {
3128
+ process.stdout.write("no keys stored.\n");
3129
+ return 0;
3130
+ }
3131
+ for (const id of keys) {
3132
+ const masked = maskKey(getKey(id));
3133
+ process.stdout.write(` ${pad(id, 14)} ${masked}
3134
+ `);
3135
+ }
3136
+ return 0;
3137
+ }
3138
+ async function cmdKeysSet(providerId) {
3139
+ return cmdProvidersSet(providerId);
3140
+ }
3141
+ async function cmdKeysRemove(providerId) {
3142
+ return cmdProvidersRemove(providerId);
1858
3143
  }
1859
3144
 
1860
3145
  // src/index.ts
1861
3146
  if (process.stdout.isTTY || process.env.FORCE_COLOR) {
1862
3147
  kleur2.enabled = true;
1863
3148
  }
1864
- var VERSION3 = "1.7.48";
3149
+ var VERSION3 = "1.7.49";
1865
3150
  var AUTH_FREE_COMMANDS = /* @__PURE__ */ new Set([
1866
3151
  "login",
1867
3152
  "logout",