@pentoshi/clai 2.0.8 → 2.0.10

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/tui/App.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text, useApp, useInput, useStdin } from "ink";
3
+ import chalk from "chalk";
3
4
  import { useCallback, useEffect, useMemo, useReducer, useRef, useState, } from "react";
4
5
  import { providerIds } from "../types.js";
5
6
  import { assertProvider } from "../llm/provider.js";
6
7
  import { getProvider } from "../llm/router.js";
7
- import { envValue, getProviderSecret, getSearchProviderKey, listProviderStatuses, maskSecret, setProviderSecret, setSecret, } from "../store/keys.js";
8
+ import { envValue, getProviderSecret, getSearchProviderKey, listProviderStatuses, maskSecret, setProviderSecret, setSecret, unsetProviderSecret, } from "../store/keys.js";
8
9
  import { setActiveSearchProvider } from "../store/config.js";
9
10
  import { searchProviderIds, } from "../tools/web/types.js";
10
11
  import { assertSearchProvider, searchProviders, } from "../tools/web/providers/provider.js";
@@ -838,10 +839,348 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
838
839
  });
839
840
  })().catch((error) => warn(`could not read keys: ${error instanceof Error ? error.message : String(error)}`));
840
841
  return true;
841
- case "/set":
842
- case "/unset":
842
+ case "/set": {
843
+ const parts = arg.split(/\s+/).filter(Boolean);
844
+ const providerVal = parts[0];
845
+ const keyVal = parts[1];
846
+ void (async () => {
847
+ if (!providerVal) {
848
+ const llm = await listProviderStatuses(provider);
849
+ const activeSearch = getConfig().activeSearchProvider;
850
+ const search = await Promise.all(searchProviderIds.map(async (id) => {
851
+ const secret = await getSearchProviderKey(id);
852
+ const keyless = id === "duckduckgo";
853
+ return {
854
+ provider: id,
855
+ configured: keyless || Boolean(secret.value),
856
+ maskedKey: secret.value ? maskSecret(secret.value) : undefined,
857
+ };
858
+ }));
859
+ setOverlay({
860
+ kind: "picker",
861
+ title: "Set API key for provider",
862
+ options: [
863
+ ...llm.map((status) => ({
864
+ value: `llm:${status.provider}`,
865
+ label: `${status.provider.padEnd(12)} ${status.configured ? chalk.green("✓ key set") : chalk.red("✗ no key")}${status.active ? " (active)" : ""}`,
866
+ description: status.model,
867
+ })),
868
+ ...search.map((status) => ({
869
+ value: `search:${status.provider}`,
870
+ label: `${status.provider.padEnd(12)} ${status.configured ? chalk.green("✓ key set") : chalk.red("✗ no key")}`,
871
+ description: `Search provider${status.provider === activeSearch ? " (active)" : ""}`,
872
+ })),
873
+ ],
874
+ onSelect: (val) => {
875
+ setOverlay({ kind: "none" });
876
+ void (async () => {
877
+ const isSearch = val.startsWith("search:");
878
+ const id = val.split(":")[1];
879
+ if (isSearch) {
880
+ const next = id;
881
+ if (next === "duckduckgo") {
882
+ info("duckduckgo is keyless and requires no setup");
883
+ return;
884
+ }
885
+ const secret = await getSearchProviderKey(next);
886
+ if (secret.value) {
887
+ const reset = await new Promise((resolveConfirm) => {
888
+ confirmResolver.current = resolveConfirm;
889
+ dispatch({
890
+ type: "event",
891
+ event: {
892
+ type: "confirm-request",
893
+ id: "c",
894
+ kind: "reset",
895
+ prompt: `${next} already has a key (${maskSecret(secret.value)}). Reset it?`,
896
+ },
897
+ });
898
+ });
899
+ if (!reset) {
900
+ info("cancelled");
901
+ return;
902
+ }
903
+ }
904
+ const key = await requestSecret({
905
+ title: `${next} API key`,
906
+ prompt: `Enter API key for ${next}:`,
907
+ });
908
+ if (!key) {
909
+ info("cancelled");
910
+ return;
911
+ }
912
+ await setSecret("search", next, key.trim());
913
+ info(`saved ${next} ${maskSecret(key.trim())}`);
914
+ }
915
+ else {
916
+ const next = id;
917
+ if (next === "ollama") {
918
+ const key = await requestSecret({
919
+ title: "Ollama host URL",
920
+ prompt: `Enter host URL for Ollama:`,
921
+ });
922
+ if (!key) {
923
+ info("cancelled");
924
+ return;
925
+ }
926
+ updateConfig({ ollamaHost: key.trim() });
927
+ info(`saved ollama host → ${key.trim()}`);
928
+ return;
929
+ }
930
+ const secret = await getProviderSecret(next);
931
+ if (secret.value) {
932
+ const reset = await new Promise((resolveConfirm) => {
933
+ confirmResolver.current = resolveConfirm;
934
+ dispatch({
935
+ type: "event",
936
+ event: {
937
+ type: "confirm-request",
938
+ id: "c",
939
+ kind: "reset",
940
+ prompt: `${next} already has a key (${maskSecret(secret.value)}). Reset it?`,
941
+ },
942
+ });
943
+ });
944
+ if (!reset) {
945
+ info("cancelled");
946
+ return;
947
+ }
948
+ }
949
+ const key = await requestSecret({
950
+ title: `${next} API key`,
951
+ prompt: `Enter API key for ${next}:`,
952
+ });
953
+ if (!key) {
954
+ info("cancelled");
955
+ return;
956
+ }
957
+ if (!getProvider(next).validateKey(key.trim())) {
958
+ warn(`invalid API key format for ${next}`);
959
+ return;
960
+ }
961
+ await setProviderSecret(next, key.trim());
962
+ info(`saved ${next} ${maskSecret(key.trim())}`);
963
+ }
964
+ })();
965
+ },
966
+ });
967
+ }
968
+ else {
969
+ try {
970
+ const isSearch = ["brave", "tavily", "duckduckgo"].includes(providerVal);
971
+ if (isSearch) {
972
+ const next = providerVal;
973
+ if (next === "duckduckgo") {
974
+ info("duckduckgo is keyless and requires no setup");
975
+ return;
976
+ }
977
+ let key = keyVal;
978
+ if (!key) {
979
+ const secret = await getSearchProviderKey(next);
980
+ if (secret.value) {
981
+ const reset = await new Promise((resolveConfirm) => {
982
+ confirmResolver.current = resolveConfirm;
983
+ dispatch({
984
+ type: "event",
985
+ event: {
986
+ type: "confirm-request",
987
+ id: "c",
988
+ kind: "reset",
989
+ prompt: `${next} already has a key (${maskSecret(secret.value)}). Reset it?`,
990
+ },
991
+ });
992
+ });
993
+ if (!reset) {
994
+ info("cancelled");
995
+ return;
996
+ }
997
+ }
998
+ key = await requestSecret({
999
+ title: `${next} API key`,
1000
+ prompt: `Enter API key for ${next}:`,
1001
+ });
1002
+ if (!key) {
1003
+ info("cancelled");
1004
+ return;
1005
+ }
1006
+ }
1007
+ await setSecret("search", next, key.trim());
1008
+ info(`saved ${next} ${maskSecret(key.trim())}`);
1009
+ }
1010
+ else {
1011
+ const next = assertProvider(providerVal);
1012
+ if (next === "ollama") {
1013
+ let key = keyVal;
1014
+ if (!key) {
1015
+ key = await requestSecret({
1016
+ title: "Ollama host URL",
1017
+ prompt: `Enter host URL for Ollama:`,
1018
+ });
1019
+ if (!key) {
1020
+ info("cancelled");
1021
+ return;
1022
+ }
1023
+ }
1024
+ updateConfig({ ollamaHost: key.trim() });
1025
+ info(`saved ollama host → ${key.trim()}`);
1026
+ return;
1027
+ }
1028
+ let key = keyVal;
1029
+ if (!key) {
1030
+ const secret = await getProviderSecret(next);
1031
+ if (secret.value) {
1032
+ const reset = await new Promise((resolveConfirm) => {
1033
+ confirmResolver.current = resolveConfirm;
1034
+ dispatch({
1035
+ type: "event",
1036
+ event: {
1037
+ type: "confirm-request",
1038
+ id: "c",
1039
+ kind: "reset",
1040
+ prompt: `${next} already has a key (${maskSecret(secret.value)}). Reset it?`,
1041
+ },
1042
+ });
1043
+ });
1044
+ if (!reset) {
1045
+ info("cancelled");
1046
+ return;
1047
+ }
1048
+ }
1049
+ key = await requestSecret({
1050
+ title: `${next} API key`,
1051
+ prompt: `Enter API key for ${next}:`,
1052
+ });
1053
+ if (!key) {
1054
+ info("cancelled");
1055
+ return;
1056
+ }
1057
+ }
1058
+ if (!getProvider(next).validateKey(key.trim())) {
1059
+ warn(`invalid API key format for ${next}`);
1060
+ return;
1061
+ }
1062
+ await setProviderSecret(next, key.trim());
1063
+ info(`saved ${next} ${maskSecret(key.trim())}`);
1064
+ }
1065
+ }
1066
+ catch (e) {
1067
+ warn(e.message);
1068
+ }
1069
+ }
1070
+ })();
1071
+ return true;
1072
+ }
1073
+ case "/unset": {
1074
+ const parts = arg.split(/\s+/).filter(Boolean);
1075
+ const providerVal = parts[0];
1076
+ void (async () => {
1077
+ if (!providerVal) {
1078
+ const llm = await listProviderStatuses(provider);
1079
+ const activeSearch = getConfig().activeSearchProvider;
1080
+ const search = await Promise.all(searchProviderIds.map(async (id) => {
1081
+ const secret = await getSearchProviderKey(id);
1082
+ const keyless = id === "duckduckgo";
1083
+ return {
1084
+ provider: id,
1085
+ configured: keyless || Boolean(secret.value),
1086
+ maskedKey: secret.value ? maskSecret(secret.value) : undefined,
1087
+ };
1088
+ }));
1089
+ setOverlay({
1090
+ kind: "picker",
1091
+ title: "Unset API key for provider",
1092
+ options: [
1093
+ ...llm.map((status) => ({
1094
+ value: `llm:${status.provider}`,
1095
+ label: `${status.provider.padEnd(12)} ${status.configured ? chalk.green("✓ ") + (status.maskedKey ?? "key set") : chalk.red("✗ no key")}${status.active ? " (active)" : ""}`,
1096
+ description: status.model,
1097
+ })),
1098
+ ...search.map((status) => ({
1099
+ value: `search:${status.provider}`,
1100
+ label: `${status.provider.padEnd(12)} ${status.configured ? chalk.green("✓ ") + (status.maskedKey ?? "keyless") : chalk.red("✗ no key")}`,
1101
+ description: `Search provider${status.provider === activeSearch ? " (active)" : ""}`,
1102
+ })),
1103
+ ],
1104
+ onSelect: (val) => {
1105
+ setOverlay({ kind: "none" });
1106
+ void (async () => {
1107
+ const isSearch = val.startsWith("search:");
1108
+ const id = val.split(":")[1];
1109
+ if (isSearch) {
1110
+ const next = id;
1111
+ if (next === "duckduckgo") {
1112
+ info("duckduckgo requires no credentials and cannot be unset");
1113
+ return;
1114
+ }
1115
+ const secret = await getSearchProviderKey(next);
1116
+ if (!secret.value) {
1117
+ warn(`${next} has no key to unset`);
1118
+ return;
1119
+ }
1120
+ const { unsetSearchProviderKey } = await import("../commands/search-providers.js");
1121
+ await unsetSearchProviderKey(next);
1122
+ info(`unset ${next}`);
1123
+ }
1124
+ else {
1125
+ const next = id;
1126
+ if (next === "ollama") {
1127
+ info("ollama does not store an API key");
1128
+ return;
1129
+ }
1130
+ const secret = await getProviderSecret(next);
1131
+ if (!secret.value) {
1132
+ warn(`${next} has no key to unset`);
1133
+ return;
1134
+ }
1135
+ await unsetProviderSecret(next);
1136
+ info(`unset ${next}`);
1137
+ }
1138
+ })();
1139
+ },
1140
+ });
1141
+ }
1142
+ else {
1143
+ try {
1144
+ const isSearch = ["brave", "tavily", "duckduckgo"].includes(providerVal);
1145
+ if (isSearch) {
1146
+ const next = providerVal;
1147
+ if (next === "duckduckgo") {
1148
+ info("duckduckgo requires no credentials and cannot be unset");
1149
+ return;
1150
+ }
1151
+ const secret = await getSearchProviderKey(next);
1152
+ if (!secret.value) {
1153
+ warn(`${next} has no key to unset`);
1154
+ return;
1155
+ }
1156
+ const { unsetSearchProviderKey } = await import("../commands/search-providers.js");
1157
+ await unsetSearchProviderKey(next);
1158
+ info(`unset ${next}`);
1159
+ }
1160
+ else {
1161
+ const next = assertProvider(providerVal);
1162
+ if (next === "ollama") {
1163
+ info("ollama does not store an API key");
1164
+ return;
1165
+ }
1166
+ const secret = await getProviderSecret(next);
1167
+ if (!secret.value) {
1168
+ warn(`${next} has no key to unset`);
1169
+ return;
1170
+ }
1171
+ await unsetProviderSecret(next);
1172
+ info(`unset ${next}`);
1173
+ }
1174
+ }
1175
+ catch (e) {
1176
+ warn(e.message);
1177
+ }
1178
+ }
1179
+ })();
1180
+ return true;
1181
+ }
843
1182
  case "/update":
844
- info(`${cmd} manages external credentials or updates; use the equivalent \`clai ${cmd.slice(1)}\` command outside the TUI`);
1183
+ info(`${cmd} manages updates; use the equivalent \`clai update\` command outside the TUI`);
845
1184
  return true;
846
1185
  case "/help":
847
1186
  setOverlay({
@@ -886,6 +1225,8 @@ export function App({ version, initialMode, provider: initialProvider, initialMo
886
1225
  openToolOutput,
887
1226
  setReasoning,
888
1227
  state.items,
1228
+ requestSecret,
1229
+ setOverlay,
889
1230
  ]);
890
1231
  const submitText = useCallback((text) => {
891
1232
  const trimmed = text.trim();