hoomanjs 1.44.0 → 1.45.0

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.
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useState } from "react";
3
3
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { z } from "zod";
4
5
  import { Box, Text, useApp, useInput } from "ink";
5
6
  import { LlmProvider, } from "../core/config.js";
6
7
  import { McpOAuthConfigSchema } from "../core/mcp/oauth/types.js";
@@ -58,7 +59,7 @@ const MCP_REMOTE_BASE_FIELDS = [
58
59
  },
59
60
  {
60
61
  key: "oauthEnabled",
61
- label: "Enable OAuth? (yes/no)",
62
+ label: "Enable OAuth",
62
63
  placeholder: "no",
63
64
  note: "Choose yes for servers that use OAuth 2.0/2.1 or dynamic client registration.",
64
65
  },
@@ -270,7 +271,7 @@ const PROVIDER_FIELD_DEFINITIONS = {
270
271
  label: "Deployment-based URLs",
271
272
  kind: "optionalBoolean",
272
273
  placeholder: "false",
273
- note: "Allowed: yes/no/true/false. Leave blank to use the AI SDK default.",
274
+ note: "Toggle between yes and no. Leave unset to use the AI SDK default.",
274
275
  },
275
276
  {
276
277
  key: "reasoningEffort",
@@ -739,6 +740,26 @@ function formatPromptCacheSummary(value) {
739
740
  }
740
741
  /** On/off display for tool rows (`Tool • Yes` / `Tool • No`). */
741
742
  const yesNo = (on) => (on ? "Yes" : "No");
743
+ function formatMcpScope(scope) {
744
+ return scope === "global" ? "global" : "project";
745
+ }
746
+ function formatMcpWriteTargetLabel(path) {
747
+ return truncate(path, 72);
748
+ }
749
+ function formatMcpDeleteDescription(name, sourcePath, scope) {
750
+ return `Remove "${name}" from ${formatMcpScope(scope)} mcp.json (${truncate(sourcePath, 64)})? This cannot be undone from here.`;
751
+ }
752
+ function formatConfigureError(error) {
753
+ if (error instanceof z.ZodError) {
754
+ const issue = error.issues[0];
755
+ if (!issue) {
756
+ return "Invalid value.";
757
+ }
758
+ const path = issue.path.map(String).join(".");
759
+ return path ? `${path}: ${issue.message}` : issue.message;
760
+ }
761
+ return error instanceof Error ? error.message : String(error);
762
+ }
742
763
  export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, }) {
743
764
  const { exit } = useApp();
744
765
  const [screen, setScreen] = useState({ kind: "home" });
@@ -747,27 +768,33 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
747
768
  const [busyMessage, setBusyMessage] = useState(null);
748
769
  const [revision, setRevision] = useState(0);
749
770
  const [mcpDraft, setMcpDraft] = useState(null);
771
+ const [providerDraft, setProviderDraft] = useState(null);
772
+ const [providerDraftType, setProviderDraftType] = useState(null);
773
+ const [llmDraft, setLlmDraft] = useState(null);
750
774
  const [installedSkills, setInstalledSkills] = useState([]);
751
775
  const [searchResults, setSearchResults] = useState([]);
752
776
  const [mcpAuthStatuses, setMcpAuthStatuses] = useState({});
753
777
  const refresh = useCallback(() => {
754
778
  setRevision((value) => value + 1);
755
779
  }, []);
780
+ const handleActionError = useCallback((error) => {
781
+ setNotice({
782
+ kind: "error",
783
+ text: formatConfigureError(error),
784
+ });
785
+ }, []);
756
786
  const runTask = useCallback(async (label, task) => {
757
787
  setBusyMessage(label);
758
788
  try {
759
789
  await task();
760
790
  }
761
791
  catch (error) {
762
- setNotice({
763
- kind: "error",
764
- text: error instanceof Error ? error.message : String(error),
765
- });
792
+ handleActionError(error);
766
793
  }
767
794
  finally {
768
795
  setBusyMessage(null);
769
796
  }
770
- }, []);
797
+ }, [handleActionError]);
771
798
  const refreshSkills = useCallback(async (label = "Loading installed skills...") => {
772
799
  await runTask(label, async () => {
773
800
  const next = await skills.list();
@@ -808,7 +835,7 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
808
835
  compaction: config.compaction,
809
836
  reasoning: config.reasoning,
810
837
  }), [config, revision]);
811
- const mcpServers = useMemo(() => mcpConfig.list(), [mcpConfig, revision]);
838
+ const mcpServers = useMemo(() => mcpConfig.listWithSources(), [mcpConfig, revision]);
812
839
  useInput((input, key) => {
813
840
  if (key.ctrl && input.toLowerCase() === "c") {
814
841
  onExit();
@@ -829,7 +856,27 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
829
856
  setScreen({ kind: "mcp" });
830
857
  return;
831
858
  }
832
- if (screen.kind === "mcp-delete-confirm") {
859
+ if (screen.kind === "config-provider-create") {
860
+ setProviderDraft(null);
861
+ setProviderDraftType(null);
862
+ setScreen({ kind: "config-providers" });
863
+ return;
864
+ }
865
+ if (screen.kind === "config-provider-create-type") {
866
+ setScreen({ kind: "config-provider-create" });
867
+ return;
868
+ }
869
+ if (screen.kind === "config-llm-create") {
870
+ setLlmDraft(null);
871
+ setScreen({ kind: "config-llms" });
872
+ return;
873
+ }
874
+ if (screen.kind === "config-llm-create-provider") {
875
+ setScreen({ kind: "config-llm-create" });
876
+ return;
877
+ }
878
+ if (screen.kind === "mcp-delete-confirm" ||
879
+ screen.kind === "mcp-save-target") {
833
880
  setScreen({ kind: "mcp" });
834
881
  return;
835
882
  }
@@ -857,6 +904,10 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
857
904
  setScreen({ kind: "config-providers" });
858
905
  return;
859
906
  }
907
+ if (screen.kind === "config-providers" || screen.kind === "config-llms") {
908
+ setScreen({ kind: "config" });
909
+ return;
910
+ }
860
911
  if (screen.kind !== "home") {
861
912
  setScreen({ kind: "home" });
862
913
  return;
@@ -905,22 +956,8 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
905
956
  : m), [config]);
906
957
  const renameLlm = useCallback((oldName, newName) => config.llms.map((m) => m.name === oldName ? { ...m, name: newName } : m), [config]);
907
958
  const setDefaultLlm = useCallback((name) => config.llms.map((m) => ({ ...m, default: m.name === name })), [config]);
908
- const addLlm = useCallback((name) => {
909
- const providerName = config.providers[0]?.name ?? "llama.cpp";
910
- const providerType = config.providers.find((provider) => provider.name === providerName)
911
- ?.provider ?? LlmProvider.LlamaCpp;
912
- return [
913
- ...config.llms,
914
- {
915
- name,
916
- provider: providerName,
917
- options: {
918
- model: defaultModelForProviderType(providerType),
919
- },
920
- default: false,
921
- },
922
- ];
923
- }, [config]);
959
+ const addLlmEntry = useCallback((entry) => [...config.llms, entry], [config]);
960
+ const addProviderEntry = useCallback((entry) => [...config.providers, entry], [config]);
924
961
  const removeLlm = useCallback((name) => config.llms.filter((m) => m.name !== name), [config]);
925
962
  const patchProvider = useCallback((name, patch) => config.providers.map((provider) => provider.name === name
926
963
  ? {
@@ -951,6 +988,12 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
951
988
  },
952
989
  ], [config]);
953
990
  const removeProvider = useCallback((name) => config.providers.filter((provider) => provider.name !== name), [config]);
991
+ const updateProviderDraftField = useCallback((key, value) => {
992
+ setProviderDraft((current) => current ? { ...current, [key]: value } : current);
993
+ }, []);
994
+ const updateLlmDraftField = useCallback((key, value) => {
995
+ setLlmDraft((current) => current ? { ...current, [key]: value } : current);
996
+ }, []);
954
997
  const promptValue = useCallback((state) => {
955
998
  setPrompt(state);
956
999
  }, []);
@@ -962,31 +1005,33 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
962
1005
  await prompt.onSubmit(value);
963
1006
  }
964
1007
  catch (error) {
965
- setNotice({
966
- kind: "error",
967
- text: error instanceof Error ? error.message : String(error),
968
- });
1008
+ handleActionError(error);
969
1009
  }
970
- }, [prompt]);
971
- const persistMcpTransport = useCallback((currentName, nextName, transport) => {
1010
+ }, [handleActionError, prompt]);
1011
+ const persistMcpTransport = useCallback((currentName, nextName, transport, targetPath) => {
972
1012
  if (!nextName) {
973
1013
  throw new Error("Server name is required.");
974
1014
  }
975
- if (currentName && currentName !== nextName && mcpConfig.get(nextName)) {
976
- throw new Error(`MCP server "${nextName}" already exists.`);
977
- }
978
1015
  if (!currentName) {
979
- mcpConfig.add(nextName, transport);
1016
+ if (!targetPath) {
1017
+ throw new Error("Save target is required for a new MCP server.");
1018
+ }
1019
+ mcpConfig.addToPath(targetPath, nextName, transport);
980
1020
  setSuccess(`Added MCP server "${nextName}".`);
981
1021
  }
982
- else if (currentName === nextName) {
983
- mcpConfig.update(currentName, transport);
984
- setSuccess(`Updated MCP server "${currentName}".`);
985
- }
986
1022
  else {
987
- mcpConfig.add(nextName, transport);
988
- mcpConfig.remove(currentName);
989
- setSuccess(`Renamed MCP server "${currentName}" to "${nextName}".`);
1023
+ const currentEntry = mcpConfig.getEntry(currentName);
1024
+ if (!currentEntry) {
1025
+ throw new Error(`MCP server "${currentName}" does not exist.`);
1026
+ }
1027
+ if (currentName === nextName) {
1028
+ mcpConfig.updateInPath(currentEntry.sourcePath, currentName, transport);
1029
+ setSuccess(`Updated MCP server "${currentName}".`);
1030
+ }
1031
+ else {
1032
+ mcpConfig.renameInPath(currentEntry.sourcePath, currentName, nextName, transport);
1033
+ setSuccess(`Renamed MCP server "${currentName}" to "${nextName}".`);
1034
+ }
990
1035
  }
991
1036
  setMcpDraft(null);
992
1037
  setScreen({ kind: "mcp" });
@@ -1046,7 +1091,32 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
1046
1091
  },
1047
1092
  });
1048
1093
  }, [promptValue, updateMcpDraftField]);
1049
- const saveMcpStdioDraft = useCallback((originalName) => {
1094
+ const editProviderDraftField = useCallback((definition, initialValue) => {
1095
+ promptValue({
1096
+ title: `Update ${definition.label}`,
1097
+ label: definition.label,
1098
+ initialValue,
1099
+ placeholder: definition.placeholder,
1100
+ note: definition.note,
1101
+ onSubmit: async (value) => {
1102
+ updateProviderDraftField(definition.key, value);
1103
+ setPrompt(null);
1104
+ },
1105
+ });
1106
+ }, [promptValue, updateProviderDraftField]);
1107
+ const editLlmDraftField = useCallback((label, key, initialValue, note) => {
1108
+ promptValue({
1109
+ title: `Update ${label}`,
1110
+ label,
1111
+ initialValue,
1112
+ note,
1113
+ onSubmit: async (value) => {
1114
+ updateLlmDraftField(key, value);
1115
+ setPrompt(null);
1116
+ },
1117
+ });
1118
+ }, [promptValue, updateLlmDraftField]);
1119
+ const buildMcpStdioDraft = useCallback(() => {
1050
1120
  const values = mcpDraft ?? {};
1051
1121
  const name = (values.name ?? "").trim();
1052
1122
  const command = (values.command ?? "").trim();
@@ -1066,9 +1136,9 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
1066
1136
  ...(env && Object.keys(env).length > 0 ? { env } : {}),
1067
1137
  ...(cwd ? { cwd } : {}),
1068
1138
  });
1069
- persistMcpTransport(originalName, name, transport);
1070
- }, [mcpDraft, persistMcpTransport]);
1071
- const saveMcpRemoteDraft = useCallback((transportType, originalName) => {
1139
+ return { name, transport };
1140
+ }, [mcpDraft]);
1141
+ const buildMcpRemoteDraft = useCallback((transportType) => {
1072
1142
  const values = mcpDraft ?? {};
1073
1143
  const name = (values.name ?? "").trim();
1074
1144
  const url = (values.url ?? "").trim();
@@ -1133,8 +1203,24 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
1133
1203
  ...(headers && Object.keys(headers).length > 0 ? { headers } : {}),
1134
1204
  ...(oauth ? { oauth } : {}),
1135
1205
  });
1206
+ return { name, transport };
1207
+ }, [mcpDraft]);
1208
+ const saveMcpStdioDraft = useCallback((originalName) => {
1209
+ const { name, transport } = buildMcpStdioDraft();
1210
+ if (!originalName) {
1211
+ setScreen({ kind: "mcp-save-target", transportType: "stdio" });
1212
+ return;
1213
+ }
1136
1214
  persistMcpTransport(originalName, name, transport);
1137
- }, [mcpDraft, persistMcpTransport]);
1215
+ }, [buildMcpStdioDraft, persistMcpTransport]);
1216
+ const saveMcpRemoteDraft = useCallback((transportType, originalName) => {
1217
+ const { name, transport } = buildMcpRemoteDraft(transportType);
1218
+ if (!originalName) {
1219
+ setScreen({ kind: "mcp-save-target", transportType });
1220
+ return;
1221
+ }
1222
+ persistMcpTransport(originalName, name, transport);
1223
+ }, [buildMcpRemoteDraft, persistMcpTransport]);
1138
1224
  const llmSummary = useCallback((entry) => {
1139
1225
  const compactModelId = (model) => {
1140
1226
  if (model.length <= 23) {
@@ -1172,10 +1258,7 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
1172
1258
  setSuccess("Updated instructions.md.");
1173
1259
  }
1174
1260
  catch (error) {
1175
- setNotice({
1176
- kind: "error",
1177
- text: error instanceof Error ? error.message : String(error),
1178
- });
1261
+ handleActionError(error);
1179
1262
  }
1180
1263
  },
1181
1264
  },
@@ -1199,7 +1282,7 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
1199
1282
  },
1200
1283
  },
1201
1284
  ];
1202
- return (_jsx(MenuScreen, { title: configData.name, description: `models: ${llmSummary(config.llms.find((m) => m.default) ?? config.llms[0])}`, items: items, footerHint: "enter: select | esc: back" }));
1285
+ return (_jsx(MenuScreen, { title: configData.name, description: `models: ${llmSummary(config.llms.find((m) => m.default) ?? config.llms[0])}`, items: items, footerHint: "enter: select | esc: back", onActionError: handleActionError }));
1203
1286
  };
1204
1287
  const renderGeneralMenu = () => {
1205
1288
  const enabledPrompts = Object.values(configData.prompts).filter(Boolean).length;
@@ -1287,7 +1370,7 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
1287
1370
  value: () => setScreen({ kind: "home" }),
1288
1371
  },
1289
1372
  ];
1290
- return (_jsx(MenuScreen, { title: "General", description: "Manage app-wide settings loaded from ~/.hooman/config.json.", items: items }));
1373
+ return (_jsx(MenuScreen, { title: "General", description: "Manage app-wide settings loaded from ~/.hooman/config.json.", items: items, onActionError: handleActionError }));
1291
1374
  };
1292
1375
  const renderConfigMenu = () => {
1293
1376
  const defaultLlm = config.llms.find((m) => m.default) ?? config.llms[0];
@@ -1421,30 +1504,145 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
1421
1504
  const items = [
1422
1505
  {
1423
1506
  label: "Add provider",
1424
- value: () => promptValue({
1425
- title: "Add a new provider",
1507
+ value: () => {
1508
+ setProviderDraft({ name: "" });
1509
+ setProviderDraftType(SUPPORTED_PROVIDER_TYPES[0]);
1510
+ setScreen({ kind: "config-provider-create" });
1511
+ },
1512
+ },
1513
+ ...providerItems,
1514
+ {
1515
+ label: "Back",
1516
+ value: () => setScreen({ kind: "config" }),
1517
+ },
1518
+ ];
1519
+ return (_jsx(MenuScreen, { title: "Providers", description: "Configure reusable provider credentials and shared params.", items: items }));
1520
+ };
1521
+ const renderProviderCreateMenu = () => {
1522
+ if (screen.kind !== "config-provider-create" || !providerDraft) {
1523
+ return null;
1524
+ }
1525
+ const providerType = providerDraftType ?? SUPPORTED_PROVIDER_TYPES[0];
1526
+ const providerFields = PROVIDER_FIELD_DEFINITIONS[providerType];
1527
+ const items = [
1528
+ {
1529
+ label: `Name • ${providerDraft.name?.trim() ? providerDraft.name : "not set"}`,
1530
+ value: () => editProviderDraftField({
1531
+ key: "name",
1426
1532
  label: "Name",
1533
+ kind: "string",
1427
1534
  placeholder: "openai-prod",
1428
- onSubmit: async (value) => {
1429
- const name = value.trim();
1430
- if (!name) {
1431
- throw new Error("Name is required.");
1535
+ }, providerDraft.name ?? ""),
1536
+ },
1537
+ {
1538
+ label: `Type ${providerType}`,
1539
+ value: () => setScreen({ kind: "config-provider-create-type" }),
1540
+ },
1541
+ ...providerFields.map((definition) => ({
1542
+ key: `provider-create-field:${definition.key}`,
1543
+ label: `${definition.label} • ${formatDraftFieldValue({
1544
+ key: definition.key,
1545
+ label: definition.label,
1546
+ placeholder: definition.placeholder,
1547
+ note: definition.note,
1548
+ }, providerDraft[definition.key])}`,
1549
+ value: () => {
1550
+ if (definition.kind === "optionalBoolean") {
1551
+ updateProviderDraftField(definition.key, isTruthyToggle(providerDraft[definition.key]) ? "no" : "yes");
1552
+ return;
1553
+ }
1554
+ editProviderDraftField(definition, providerDraft[definition.key] ?? "");
1555
+ },
1556
+ })),
1557
+ {
1558
+ label: "Save",
1559
+ value: () => {
1560
+ const name = (providerDraft.name ?? "").trim();
1561
+ if (!name) {
1562
+ throw new Error("Name is required.");
1563
+ }
1564
+ if (config.providers.some((provider) => provider.name === name)) {
1565
+ throw new Error(`A provider named "${name}" already exists.`);
1566
+ }
1567
+ const options = providerOptionsTemplate(providerType);
1568
+ for (const definition of providerFields) {
1569
+ if (definition.key === "openaiApi") {
1570
+ const nextValue = parseTypedFieldValue(providerDraft[definition.key] ?? "", definition);
1571
+ options.api = nextValue;
1572
+ continue;
1432
1573
  }
1433
- if (config.providers.some((provider) => provider.name === name)) {
1434
- throw new Error(`A provider named "${name}" already exists.`);
1574
+ if (definition.kind === "reasoningEffort") {
1575
+ const effort = parseTypedFieldValue(providerDraft[definition.key] ?? "", definition);
1576
+ const reasoning = {
1577
+ ...(options.reasoning ?? {}),
1578
+ effort,
1579
+ };
1580
+ options.reasoning = Object.values(reasoning).some((value) => value !== undefined)
1581
+ ? reasoning
1582
+ : undefined;
1583
+ continue;
1435
1584
  }
1436
- setPrompt(null);
1437
- setScreen({ kind: "config-provider-add-type", name });
1438
- },
1439
- }),
1585
+ if (definition.kind === "reasoningSummary") {
1586
+ const summary = parseTypedFieldValue(providerDraft[definition.key] ?? "", definition);
1587
+ const reasoning = {
1588
+ ...(options.reasoning ?? {}),
1589
+ summary,
1590
+ };
1591
+ options.reasoning = Object.values(reasoning).some((value) => value !== undefined)
1592
+ ? reasoning
1593
+ : undefined;
1594
+ continue;
1595
+ }
1596
+ if (definition.kind === "reasoningDisplay") {
1597
+ const display = parseTypedFieldValue(providerDraft[definition.key] ?? "", definition);
1598
+ const reasoning = {
1599
+ ...(options.reasoning ?? {}),
1600
+ display,
1601
+ };
1602
+ options.reasoning = Object.values(reasoning).some((value) => value !== undefined)
1603
+ ? reasoning
1604
+ : undefined;
1605
+ continue;
1606
+ }
1607
+ if (definition.kind === "bedrockCredentials") {
1608
+ const accessKeyId = normalizeOptional(providerDraft.accessKeyId ?? "");
1609
+ const secretAccessKey = normalizeOptional(providerDraft.secretAccessKey ?? "");
1610
+ if ((accessKeyId === undefined) !==
1611
+ (secretAccessKey === undefined)) {
1612
+ throw new Error("Access key ID and secret access key must be provided together.");
1613
+ }
1614
+ options.accessKeyId = accessKeyId;
1615
+ options.secretAccessKey = secretAccessKey;
1616
+ continue;
1617
+ }
1618
+ if (definition.kind === "promptCache") {
1619
+ continue;
1620
+ }
1621
+ options[definition.key] = parseTypedFieldValue(providerDraft[definition.key] ?? "", definition);
1622
+ }
1623
+ if (updateConfig({
1624
+ providers: addProviderEntry({
1625
+ name,
1626
+ provider: providerType,
1627
+ options: options,
1628
+ }),
1629
+ }, `Added provider "${name}" as "${providerType}".`)) {
1630
+ setProviderDraft(null);
1631
+ setProviderDraftType(null);
1632
+ setScreen({ kind: "config-provider-edit", name });
1633
+ }
1634
+ },
1440
1635
  },
1441
- ...providerItems,
1442
1636
  {
1443
1637
  label: "Back",
1444
- value: () => setScreen({ kind: "config" }),
1638
+ value: () => {
1639
+ setProviderDraft(null);
1640
+ setProviderDraftType(null);
1641
+ setScreen({ kind: "config-providers" });
1642
+ },
1445
1643
  },
1446
1644
  ];
1447
- return (_jsx(MenuScreen, { title: "Providers", description: "Configure reusable provider credentials and shared params.", items: items }));
1645
+ return (_jsx(MenuScreen, { title: "Add a new provider", description: "Select a field to edit, then save when you're done.", items: items, onActionError: handleActionError }));
1448
1646
  };
1449
1647
  const renderProviderEditMenu = () => {
1450
1648
  if (screen.kind !== "config-provider-edit") {
@@ -1537,6 +1735,16 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
1537
1735
  });
1538
1736
  return;
1539
1737
  }
1738
+ if (definition.kind === "optionalBoolean") {
1739
+ const currentValue = providerOptions[definition.key];
1740
+ const nextValue = currentValue === true ? false : true;
1741
+ updateConfig({
1742
+ providers: patchProvider(entry.name, {
1743
+ [definition.key]: nextValue,
1744
+ }),
1745
+ }, `Updated ${definition.label.toLowerCase()} for "${entry.name}" to ${nextValue ? "yes" : "no"}.`);
1746
+ return;
1747
+ }
1540
1748
  if (definition.kind === "bedrockCredentials") {
1541
1749
  promptValue({
1542
1750
  title: "Update static credentials",
@@ -1858,27 +2066,22 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
1858
2066
  const items = [
1859
2067
  {
1860
2068
  label: "Add LLM",
1861
- value: () => promptValue({
1862
- title: "Add a new LLM",
1863
- label: "Name",
1864
- placeholder: "Gemma 27B",
1865
- onSubmit: async (value) => {
1866
- const name = value.trim();
1867
- if (!name) {
1868
- throw new Error("Name is required.");
1869
- }
1870
- if (config.llms.some((m) => m.name === name)) {
1871
- throw new Error(`An LLM named "${name}" already exists.`);
1872
- }
1873
- if (config.providers.length === 0) {
1874
- throw new Error("Add at least one provider first so the model can reference it.");
1875
- }
1876
- if (updateConfig({ llms: addLlm(name) }, `Added LLM "${name}".`)) {
1877
- setPrompt(null);
1878
- setScreen({ kind: "config-llm-edit", name });
1879
- }
1880
- },
1881
- }),
2069
+ value: () => {
2070
+ if (config.providers.length === 0) {
2071
+ throw new Error("Add at least one provider first so the model can reference it.");
2072
+ }
2073
+ const providerName = config.providers[0].name;
2074
+ const providerType = config.providers[0].provider;
2075
+ setLlmDraft({
2076
+ name: "",
2077
+ provider: providerName,
2078
+ model: defaultModelForProviderType(providerType),
2079
+ temperature: "",
2080
+ maxTokens: "",
2081
+ context: "",
2082
+ });
2083
+ setScreen({ kind: "config-llm-create" });
2084
+ },
1882
2085
  },
1883
2086
  ...llmItems,
1884
2087
  {
@@ -1888,6 +2091,86 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
1888
2091
  ];
1889
2092
  return (_jsx(MenuScreen, { title: "LLMs", description: "Add, edit, or remove named LLM configurations. The default is used for new sessions.", items: items }));
1890
2093
  };
2094
+ const renderLlmCreateMenu = () => {
2095
+ if (screen.kind !== "config-llm-create" || !llmDraft) {
2096
+ return null;
2097
+ }
2098
+ const provider = config.providers.find((candidate) => candidate.name === llmDraft.provider);
2099
+ const providerType = provider?.provider;
2100
+ const items = [
2101
+ {
2102
+ label: `Name • ${llmDraft.name?.trim() ? llmDraft.name : "not set"}`,
2103
+ value: () => editLlmDraftField("Name", "name", llmDraft.name ?? ""),
2104
+ },
2105
+ {
2106
+ label: `Provider • ${llmDraft.provider ?? "not set"}`,
2107
+ value: () => setScreen({ kind: "config-llm-create-provider" }),
2108
+ },
2109
+ {
2110
+ label: `Model • ${llmDraft.model?.trim() ? llmDraft.model : "not set"}`,
2111
+ value: () => editLlmDraftField("Model", "model", llmDraft.model ?? "", providerType
2112
+ ? `Suggested default for ${providerType}: ${defaultModelForProviderType(providerType)}`
2113
+ : undefined),
2114
+ },
2115
+ ...LLM_FIELD_DEFINITIONS.map((definition) => ({
2116
+ key: `llm-create-field:${definition.key}`,
2117
+ label: `${definition.label} • ${formatDraftFieldValue({
2118
+ key: definition.key,
2119
+ label: definition.label,
2120
+ placeholder: definition.placeholder,
2121
+ note: definition.note,
2122
+ }, llmDraft[definition.key])}`,
2123
+ value: () => editLlmDraftField(definition.label, definition.key, llmDraft[definition.key] ?? "", definition.note),
2124
+ })),
2125
+ {
2126
+ label: "Save",
2127
+ value: () => {
2128
+ const name = (llmDraft.name ?? "").trim();
2129
+ if (!name) {
2130
+ throw new Error("Name is required.");
2131
+ }
2132
+ if (config.llms.some((llm) => llm.name === name)) {
2133
+ throw new Error(`An LLM named "${name}" already exists.`);
2134
+ }
2135
+ const providerName = (llmDraft.provider ?? "").trim();
2136
+ if (!providerName) {
2137
+ throw new Error("Provider is required.");
2138
+ }
2139
+ const selectedProvider = config.providers.find((candidate) => candidate.name === providerName);
2140
+ if (!selectedProvider) {
2141
+ throw new Error(`Provider "${providerName}" does not exist.`);
2142
+ }
2143
+ const model = (llmDraft.model ?? "").trim();
2144
+ if (!model) {
2145
+ throw new Error("Model is required.");
2146
+ }
2147
+ const options = { model };
2148
+ for (const definition of LLM_FIELD_DEFINITIONS) {
2149
+ options[definition.key] = parseTypedFieldValue(llmDraft[definition.key] ?? "", definition);
2150
+ }
2151
+ if (updateConfig({
2152
+ llms: addLlmEntry({
2153
+ name,
2154
+ provider: providerName,
2155
+ options: options,
2156
+ default: false,
2157
+ }),
2158
+ }, `Added LLM "${name}".`)) {
2159
+ setLlmDraft(null);
2160
+ setScreen({ kind: "config-llm-edit", name });
2161
+ }
2162
+ },
2163
+ },
2164
+ {
2165
+ label: "Back",
2166
+ value: () => {
2167
+ setLlmDraft(null);
2168
+ setScreen({ kind: "config-llms" });
2169
+ },
2170
+ },
2171
+ ];
2172
+ return (_jsx(MenuScreen, { title: "Add a new LLM", description: "Select a field to edit, then save when you're done.", items: items, onActionError: handleActionError }));
2173
+ };
1891
2174
  const renderLlmEditMenu = () => {
1892
2175
  if (screen.kind !== "config-llm-edit") {
1893
2176
  return null;
@@ -2008,6 +2291,33 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
2008
2291
  ? "This is the default LLM. Set another as default to enable deletion."
2009
2292
  : "Edit fields, set as default, or delete this LLM.", items: items }));
2010
2293
  };
2294
+ const renderLlmCreateProviderMenu = () => {
2295
+ if (screen.kind !== "config-llm-create-provider" || !llmDraft) {
2296
+ return null;
2297
+ }
2298
+ const items = [
2299
+ ...config.providers.map((provider) => ({
2300
+ label: provider.name === llmDraft.provider
2301
+ ? `${provider.name} • current`
2302
+ : `${provider.name} • ${provider.provider}`,
2303
+ value: () => {
2304
+ setLlmDraft((current) => current
2305
+ ? {
2306
+ ...current,
2307
+ provider: provider.name,
2308
+ model: defaultModelForProviderType(provider.provider),
2309
+ }
2310
+ : current);
2311
+ setScreen({ kind: "config-llm-create" });
2312
+ },
2313
+ })),
2314
+ {
2315
+ label: "Back",
2316
+ value: () => setScreen({ kind: "config-llm-create" }),
2317
+ },
2318
+ ];
2319
+ return (_jsx(MenuScreen, { title: "Choose provider", description: "Pick which shared provider config this LLM should use.", items: items, onActionError: handleActionError }));
2320
+ };
2011
2321
  const renderLlmProviderMenu = () => {
2012
2322
  if (screen.kind !== "config-llm-provider") {
2013
2323
  return null;
@@ -2254,7 +2564,7 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
2254
2564
  },
2255
2565
  },
2256
2566
  ];
2257
- return (_jsx(MenuScreen, { title: `${screen.originalName ? "Edit" : "Add"} stdio server`, description: "Select a field to edit, then save when you're done.", items: items }));
2567
+ return (_jsx(MenuScreen, { title: `${screen.originalName ? "Edit" : "Add"} stdio server`, description: "Select a field to edit, then save when you're done.", items: items, onActionError: handleActionError }));
2258
2568
  };
2259
2569
  const renderMcpRemoteEditMenu = () => {
2260
2570
  if (screen.kind !== "mcp-remote-edit" || !mcpDraft) {
@@ -2268,7 +2578,13 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
2268
2578
  ...fields.map((field) => ({
2269
2579
  key: `mcp-remote:${field.key}`,
2270
2580
  label: `${field.label} • ${formatDraftFieldValue(field, mcpDraft[field.key])}`,
2271
- value: () => editMcpDraftField(field, mcpDraft[field.key] ?? ""),
2581
+ value: () => {
2582
+ if (field.key === "oauthEnabled") {
2583
+ updateMcpDraftField(field.key, isTruthyToggle(mcpDraft[field.key]) ? "no" : "yes");
2584
+ return;
2585
+ }
2586
+ editMcpDraftField(field, mcpDraft[field.key] ?? "");
2587
+ },
2272
2588
  })),
2273
2589
  {
2274
2590
  label: "Save",
@@ -2282,14 +2598,47 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
2282
2598
  },
2283
2599
  },
2284
2600
  ];
2285
- return (_jsx(MenuScreen, { title: `${screen.originalName ? "Edit" : "Add"} ${screen.transportType} server`, description: "Select a field to edit, then save when you're done.", items: items }));
2601
+ return (_jsx(MenuScreen, { title: `${screen.originalName ? "Edit" : "Add"} ${screen.transportType} server`, description: "Select a field to edit, then save when you're done.", items: items, onActionError: handleActionError }));
2602
+ };
2603
+ const renderMcpSaveTargetMenu = () => {
2604
+ if (screen.kind !== "mcp-save-target") {
2605
+ return null;
2606
+ }
2607
+ const targets = mcpConfig.writableTargets();
2608
+ const saveToTarget = (path) => {
2609
+ try {
2610
+ if (screen.transportType === "stdio") {
2611
+ const { name, transport } = buildMcpStdioDraft();
2612
+ persistMcpTransport(undefined, name, transport, path);
2613
+ }
2614
+ else {
2615
+ const { name, transport } = buildMcpRemoteDraft(screen.transportType);
2616
+ persistMcpTransport(undefined, name, transport, path);
2617
+ }
2618
+ }
2619
+ catch (error) {
2620
+ handleActionError(error);
2621
+ }
2622
+ };
2623
+ const items = [
2624
+ ...targets.map((target) => ({
2625
+ key: `mcp-save-target:${target.path}`,
2626
+ label: `${target.scope === "global" ? "Global" : "Project"} • ${formatMcpWriteTargetLabel(target.path)}`,
2627
+ value: () => saveToTarget(target.path),
2628
+ })),
2629
+ {
2630
+ label: "Back",
2631
+ value: () => setScreen({ kind: "mcp" }),
2632
+ },
2633
+ ];
2634
+ return (_jsx(MenuScreen, { title: "Choose save target", description: "Select where to save this new MCP server.", items: items, onActionError: handleActionError }));
2286
2635
  };
2287
2636
  const renderMcpMenu = () => {
2288
2637
  const serverItems = mcpServers.map((server) => {
2289
2638
  const oauthStatus = mcpAuthStatuses[server.name];
2290
2639
  return {
2291
2640
  key: `mcp-server:${server.name}`,
2292
- label: `Edit ${server.name} • ${formatMcpServerLabel(server.transport, oauthStatus)}`,
2641
+ label: `Edit ${server.name} • ${formatMcpServerLabel(server.transport, oauthStatus)} • ${formatMcpScope(server.scope)}`,
2293
2642
  boldSubstring: server.name,
2294
2643
  oauthStatus: oauthStatus === "authenticated" ||
2295
2644
  oauthStatus === "expired" ||
@@ -2325,7 +2674,10 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
2325
2674
  value: () => {
2326
2675
  mcpConfig.reload();
2327
2676
  refresh();
2328
- setNotice({ kind: "info", text: "Reloaded MCP config from disk." });
2677
+ setNotice({
2678
+ kind: "info",
2679
+ text: "Reloaded merged MCP config from disk.",
2680
+ });
2329
2681
  },
2330
2682
  },
2331
2683
  {
@@ -2333,7 +2685,7 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
2333
2685
  value: () => setScreen({ kind: "home" }),
2334
2686
  },
2335
2687
  ];
2336
- return (_jsx(MenuScreen, { title: "MCP Servers", description: "Add, edit, or remove named MCP transports from ~/.hooman/mcp.json.", items: items, footerHint: (item) => formatMcpFooterHint(item, mcpServers, mcpAuthStatuses), onShortcut: async (input, item) => {
2688
+ return (_jsx(MenuScreen, { title: "MCP Servers", description: "Add, edit, or remove named MCP transports from merged MCP config (global + project overlays).", items: items, footerHint: (item) => formatMcpFooterHint(item, mcpServers, mcpAuthStatuses), onActionError: handleActionError, onShortcut: async (input, item) => {
2337
2689
  const server = findMcpServerFromMenuItem(item, mcpServers);
2338
2690
  if (!server) {
2339
2691
  return;
@@ -2341,7 +2693,12 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
2341
2693
  const status = mcpAuthStatuses[server.name];
2342
2694
  const key = input.toLowerCase();
2343
2695
  if (key === "d") {
2344
- setScreen({ kind: "mcp-delete-confirm", name: server.name });
2696
+ setScreen({
2697
+ kind: "mcp-delete-confirm",
2698
+ name: server.name,
2699
+ sourcePath: server.sourcePath,
2700
+ scope: server.scope,
2701
+ });
2345
2702
  return;
2346
2703
  }
2347
2704
  if (key === "r" &&
@@ -2433,7 +2790,7 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
2433
2790
  if (screen.kind !== "mcp-delete-confirm") {
2434
2791
  return null;
2435
2792
  }
2436
- const { name } = screen;
2793
+ const { name, sourcePath, scope } = screen;
2437
2794
  const items = [
2438
2795
  {
2439
2796
  key: `mcp-del-cancel:${name}`,
@@ -2442,24 +2799,21 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
2442
2799
  },
2443
2800
  {
2444
2801
  key: `mcp-del-confirm:${name}`,
2445
- label: "Yes — remove from mcp.json",
2802
+ label: `Yes — remove from ${formatMcpScope(scope)} mcp.json`,
2446
2803
  value: () => {
2447
2804
  try {
2448
- mcpConfig.remove(name);
2805
+ mcpConfig.removeFromPath(sourcePath, name);
2449
2806
  refresh();
2450
- setSuccess(`Deleted MCP server "${name}".`);
2807
+ setSuccess(`Deleted MCP server "${name}" from ${formatMcpScope(scope)} mcp.json.`);
2451
2808
  }
2452
2809
  catch (error) {
2453
- setNotice({
2454
- kind: "error",
2455
- text: error instanceof Error ? error.message : String(error),
2456
- });
2810
+ handleActionError(error);
2457
2811
  }
2458
2812
  setScreen({ kind: "mcp" });
2459
2813
  },
2460
2814
  },
2461
2815
  ];
2462
- return (_jsx(MenuScreen, { title: "Delete MCP server?", description: `Remove "${name}" from ~/.hooman/mcp.json? This cannot be undone from here.`, items: items }));
2816
+ return (_jsx(MenuScreen, { title: "Delete MCP server?", description: formatMcpDeleteDescription(name, sourcePath, scope), items: items, onActionError: handleActionError }));
2463
2817
  };
2464
2818
  const renderSkillsDeleteConfirm = () => {
2465
2819
  if (screen.kind !== "skills-delete-confirm") {
@@ -2525,6 +2879,10 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
2525
2879
  return renderConfigMenu();
2526
2880
  case "config-providers":
2527
2881
  return renderProvidersMenu();
2882
+ case "config-provider-create":
2883
+ return renderProviderCreateMenu();
2884
+ case "config-provider-create-type":
2885
+ return renderProviderAddTypeMenu();
2528
2886
  case "config-provider-add-type":
2529
2887
  return renderProviderAddTypeMenu();
2530
2888
  case "config-provider-edit":
@@ -2545,6 +2903,10 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
2545
2903
  return renderProviderDeleteConfirm();
2546
2904
  case "config-llms":
2547
2905
  return renderLlmsMenu();
2906
+ case "config-llm-create":
2907
+ return renderLlmCreateMenu();
2908
+ case "config-llm-create-provider":
2909
+ return renderLlmCreateProviderMenu();
2548
2910
  case "config-llm-edit":
2549
2911
  return renderLlmEditMenu();
2550
2912
  case "config-llm-provider":
@@ -2561,6 +2923,8 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
2561
2923
  return renderSearchProviderMenu();
2562
2924
  case "mcp":
2563
2925
  return renderMcpMenu();
2926
+ case "mcp-save-target":
2927
+ return renderMcpSaveTargetMenu();
2564
2928
  case "mcp-stdio-edit":
2565
2929
  return renderMcpStdioEditMenu();
2566
2930
  case "mcp-remote-edit":