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.
- package/dist/index.js +2299 -1014
- 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
|
|
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
|
|
131
|
-
resolvedPath =
|
|
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
|
|
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/
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
-
|
|
705
|
-
|
|
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
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
|
|
735
|
-
|
|
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
|
-
|
|
951
|
+
detail = (await res.json())?.detail || "";
|
|
743
952
|
} catch {
|
|
953
|
+
detail = await res.text();
|
|
744
954
|
}
|
|
745
|
-
|
|
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
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
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
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
|
|
831
|
-
return
|
|
1558
|
+
re += "$";
|
|
1559
|
+
return new RegExp(re);
|
|
832
1560
|
}
|
|
833
1561
|
|
|
834
|
-
// src/
|
|
835
|
-
|
|
836
|
-
|
|
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
|
|
841
|
-
|
|
1566
|
+
async function nodeCp() {
|
|
1567
|
+
return import("node:child_process");
|
|
842
1568
|
}
|
|
843
|
-
function
|
|
844
|
-
|
|
1569
|
+
async function nodeOs() {
|
|
1570
|
+
return import("node:os");
|
|
845
1571
|
}
|
|
846
|
-
function
|
|
847
|
-
return
|
|
1572
|
+
async function nodeNet() {
|
|
1573
|
+
return import("node:net");
|
|
848
1574
|
}
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
-
|
|
878
|
-
|
|
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
|
-
|
|
894
|
-
|
|
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
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
);
|
|
1602
|
+
async unlink(absPath) {
|
|
1603
|
+
const fs = await nodeFs();
|
|
1604
|
+
await fs.rm(absPath, { force: true });
|
|
940
1605
|
}
|
|
941
|
-
|
|
942
|
-
|
|
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
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
|
|
973
|
-
|
|
974
|
-
)
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
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
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
-
|
|
1025
|
-
|
|
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
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
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/
|
|
1157
|
-
import { existsSync as
|
|
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
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1178
|
-
if (!
|
|
1179
|
-
return
|
|
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
|
-
|
|
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
|
|
1196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1251
|
-
return
|
|
1810
|
+
function genVerifier() {
|
|
1811
|
+
return b64url(crypto.randomBytes(48));
|
|
1252
1812
|
}
|
|
1253
|
-
function
|
|
1254
|
-
|
|
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
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
-
|
|
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
|
|
1273
|
-
const
|
|
1274
|
-
if (
|
|
1275
|
-
return
|
|
1276
|
-
|
|
1277
|
-
|
|
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
|
|
1280
|
-
|
|
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
|
|
1853
|
+
return JSON.parse(raw);
|
|
1283
1854
|
} catch {
|
|
1284
|
-
return
|
|
1855
|
+
return null;
|
|
1285
1856
|
}
|
|
1286
1857
|
}
|
|
1287
|
-
function
|
|
1288
|
-
|
|
1858
|
+
function storeProfile(p) {
|
|
1859
|
+
if (!p)
|
|
1860
|
+
return;
|
|
1861
|
+
writeSecret(pathProfile(), JSON.stringify(p, null, 2));
|
|
1289
1862
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
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
|
|
1300
|
-
|
|
1872
|
+
function clearTokens() {
|
|
1873
|
+
rmIf(pathAccess());
|
|
1874
|
+
rmIf(pathRefresh());
|
|
1875
|
+
rmIf(pathAccessExp());
|
|
1876
|
+
rmIf(pathProfile());
|
|
1301
1877
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
return
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
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
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
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
|
-
|
|
1343
|
-
return
|
|
2564
|
+
nl();
|
|
2565
|
+
return p;
|
|
1344
2566
|
}
|
|
1345
|
-
async function
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
if (
|
|
1349
|
-
|
|
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
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
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
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
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
|
|
1364
|
-
process.
|
|
1365
|
-
|
|
1366
|
-
return
|
|
2711
|
+
const envVar = cfg.apiKeyEnvVar;
|
|
2712
|
+
if (envVar && (process.env[envVar] ?? "").length > 8)
|
|
2713
|
+
return true;
|
|
2714
|
+
return false;
|
|
1367
2715
|
}
|
|
1368
|
-
async function
|
|
1369
|
-
const
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
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
|
|
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
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
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
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
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
|
-
|
|
1411
|
-
`));
|
|
2774
|
+
println(palette.error("\n install failed: " + (err instanceof Error ? err.message : String(err))));
|
|
1412
2775
|
return 1;
|
|
1413
2776
|
}
|
|
1414
2777
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
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
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
return
|
|
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
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
const
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
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
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
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
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
return
|
|
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/
|
|
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
|
-
|
|
1526
|
-
|
|
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
|
|
1542
|
-
return join6(
|
|
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
|
|
1560
|
-
|
|
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
|
|
2885
|
+
function getKey(providerId) {
|
|
2886
|
+
const path = keyFile(providerId);
|
|
1567
2887
|
if (!existsSync7(path))
|
|
1568
|
-
return
|
|
2888
|
+
return "";
|
|
1569
2889
|
try {
|
|
1570
2890
|
return readFileSync5(path, "utf8").trim();
|
|
1571
2891
|
} catch {
|
|
1572
|
-
return
|
|
2892
|
+
return "";
|
|
1573
2893
|
}
|
|
1574
2894
|
}
|
|
1575
|
-
function
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
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
|
|
1595
|
-
const
|
|
1596
|
-
if (parts.length < 2)
|
|
1597
|
-
return null;
|
|
2902
|
+
function listKeys() {
|
|
2903
|
+
const d = keysDir();
|
|
1598
2904
|
try {
|
|
1599
|
-
|
|
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
|
|
2907
|
+
return [];
|
|
1604
2908
|
}
|
|
1605
2909
|
}
|
|
1606
|
-
function
|
|
1607
|
-
|
|
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
|
-
|
|
1624
|
-
|
|
2913
|
+
|
|
2914
|
+
// src/commands/providers.ts
|
|
2915
|
+
function maskKey(raw) {
|
|
1625
2916
|
if (!raw)
|
|
1626
|
-
return
|
|
1627
|
-
|
|
1628
|
-
return
|
|
1629
|
-
|
|
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
|
-
|
|
1669
|
-
|
|
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
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
const
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
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
|
-
|
|
2936
|
+
process.stdin.pause();
|
|
1698
2937
|
} catch {
|
|
1699
2938
|
}
|
|
1700
|
-
|
|
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
|
-
|
|
1740
|
-
|
|
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
|
-
|
|
2944
|
+
process.stdin.resume();
|
|
1759
2945
|
} catch {
|
|
1760
|
-
detail = await res.text();
|
|
1761
2946
|
}
|
|
1762
|
-
|
|
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
|
-
|
|
2965
|
+
process.stdout.write("\n");
|
|
2966
|
+
return 0;
|
|
1765
2967
|
}
|
|
1766
|
-
function
|
|
1767
|
-
const
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
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
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
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
|
|
1787
|
-
const
|
|
1788
|
-
|
|
1789
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
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
|
-
|
|
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
|
|
1826
|
-
const
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
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
|
-
|
|
3097
|
+
process.stdout.write("\n");
|
|
3098
|
+
return 0;
|
|
1839
3099
|
}
|
|
1840
|
-
async function
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
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
|
-
|
|
1851
|
-
const
|
|
1852
|
-
if (
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
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.
|
|
3149
|
+
var VERSION3 = "1.7.49";
|
|
1865
3150
|
var AUTH_FREE_COMMANDS = /* @__PURE__ */ new Set([
|
|
1866
3151
|
"login",
|
|
1867
3152
|
"logout",
|