hoomanjs 1.33.0 → 1.35.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.
Files changed (158) hide show
  1. package/README.md +114 -270
  2. package/dist/acp/acp-agent.js +2 -16
  3. package/dist/acp/acp-agent.js.map +1 -1
  4. package/dist/acp/sessions/options.js +2 -2
  5. package/dist/acp/sessions/options.js.map +1 -1
  6. package/dist/acp/sessions/store.d.ts +0 -2
  7. package/dist/acp/sessions/store.js.map +1 -1
  8. package/dist/acp/utils/tool-kind.js +10 -2
  9. package/dist/acp/utils/tool-kind.js.map +1 -1
  10. package/dist/chat/app.d.ts +2 -1
  11. package/dist/chat/app.js +94 -23
  12. package/dist/chat/app.js.map +1 -1
  13. package/dist/chat/components/BottomChrome.d.ts +6 -2
  14. package/dist/chat/components/BottomChrome.js +2 -2
  15. package/dist/chat/components/BottomChrome.js.map +1 -1
  16. package/dist/chat/components/ChromePicker.d.ts +7 -2
  17. package/dist/chat/components/ChromePicker.js +9 -2
  18. package/dist/chat/components/ChromePicker.js.map +1 -1
  19. package/dist/chat/components/StatusBar.d.ts +1 -2
  20. package/dist/chat/components/StatusBar.js +3 -12
  21. package/dist/chat/components/StatusBar.js.map +1 -1
  22. package/dist/chat/components/ToolEvent.js +1 -1
  23. package/dist/chat/components/ToolEvent.js.map +1 -1
  24. package/dist/chat/index.d.ts +2 -1
  25. package/dist/chat/index.js +7 -0
  26. package/dist/chat/index.js.map +1 -1
  27. package/dist/cli.js +120 -4
  28. package/dist/cli.js.map +1 -1
  29. package/dist/configure/app.js +1005 -392
  30. package/dist/configure/app.js.map +1 -1
  31. package/dist/configure/index.js.map +1 -1
  32. package/dist/configure/types.d.ts +18 -0
  33. package/dist/core/agent/index.d.ts +13 -3
  34. package/dist/core/agent/index.js +83 -15
  35. package/dist/core/agent/index.js.map +1 -1
  36. package/dist/core/config.d.ts +319 -87
  37. package/dist/core/config.js +191 -218
  38. package/dist/core/config.js.map +1 -1
  39. package/dist/core/index.d.ts +1 -2
  40. package/dist/core/index.js +6 -7
  41. package/dist/core/index.js.map +1 -1
  42. package/dist/core/mcp/config.d.ts +7 -1
  43. package/dist/core/mcp/config.js +41 -8
  44. package/dist/core/mcp/config.js.map +1 -1
  45. package/dist/core/mcp/manager.js +1 -1
  46. package/dist/core/mcp/manager.js.map +1 -1
  47. package/dist/core/mcp/oauth/provider.js.map +1 -1
  48. package/dist/core/mcp/prefixed-mcp-tool.d.ts +0 -1
  49. package/dist/core/mcp/prefixed-mcp-tool.js +0 -2
  50. package/dist/core/mcp/prefixed-mcp-tool.js.map +1 -1
  51. package/dist/core/memory/index.d.ts +1 -0
  52. package/dist/core/memory/index.js +1 -0
  53. package/dist/core/memory/index.js.map +1 -1
  54. package/dist/core/memory/model-extractor.js.map +1 -0
  55. package/dist/core/models/anthropic.d.ts +3 -12
  56. package/dist/core/models/anthropic.js +20 -91
  57. package/dist/core/models/anthropic.js.map +1 -1
  58. package/dist/core/models/azure.d.ts +4 -0
  59. package/dist/core/models/azure.js +26 -0
  60. package/dist/core/models/azure.js.map +1 -0
  61. package/dist/core/models/bedrock.d.ts +3 -20
  62. package/dist/core/models/bedrock.js +22 -18
  63. package/dist/core/models/bedrock.js.map +1 -1
  64. package/dist/core/models/google.d.ts +2 -9
  65. package/dist/core/models/google.js +12 -31
  66. package/dist/core/models/google.js.map +1 -1
  67. package/dist/core/models/groq.d.ts +2 -9
  68. package/dist/core/models/groq.js +15 -25
  69. package/dist/core/models/groq.js.map +1 -1
  70. package/dist/core/models/index.d.ts +2 -1
  71. package/dist/core/models/index.js +3 -0
  72. package/dist/core/models/index.js.map +1 -1
  73. package/dist/core/models/minimax.d.ts +2 -0
  74. package/dist/core/models/minimax.js +11 -0
  75. package/dist/core/models/minimax.js.map +1 -0
  76. package/dist/core/models/moonshot.d.ts +2 -10
  77. package/dist/core/models/moonshot.js +23 -38
  78. package/dist/core/models/moonshot.js.map +1 -1
  79. package/dist/core/models/ollama/index.d.ts +2 -1
  80. package/dist/core/models/ollama/index.js +12 -5
  81. package/dist/core/models/ollama/index.js.map +1 -1
  82. package/dist/core/models/openai.d.ts +3 -4
  83. package/dist/core/models/openai.js +20 -3
  84. package/dist/core/models/openai.js.map +1 -1
  85. package/dist/core/models/openrouter.d.ts +4 -0
  86. package/dist/core/models/openrouter.js +23 -0
  87. package/dist/core/models/openrouter.js.map +1 -0
  88. package/dist/core/models/types.d.ts +346 -0
  89. package/dist/core/models/types.js +220 -0
  90. package/dist/core/models/types.js.map +1 -0
  91. package/dist/core/models/xai.d.ts +2 -9
  92. package/dist/core/models/xai.js +15 -25
  93. package/dist/core/models/xai.js.map +1 -1
  94. package/dist/core/modes/definitions.js +6 -2
  95. package/dist/core/modes/definitions.js.map +1 -1
  96. package/dist/core/prompts/agents/research.md +8 -4
  97. package/dist/core/prompts/agents/review.md +26 -0
  98. package/dist/core/prompts/agents/test-investigator.md +26 -0
  99. package/dist/core/prompts/modes/ask.md +1 -1
  100. package/dist/core/prompts/modes/plan.md +1 -1
  101. package/dist/core/prompts/runtime.js +2 -30
  102. package/dist/core/prompts/runtime.js.map +1 -1
  103. package/dist/core/prompts/static/subagents.md +12 -8
  104. package/dist/core/prompts/system.js +1 -1
  105. package/dist/core/prompts/system.js.map +1 -1
  106. package/dist/core/runtime-config.d.ts +16 -0
  107. package/dist/core/runtime-config.js +44 -0
  108. package/dist/core/runtime-config.js.map +1 -0
  109. package/dist/core/session-config.js +2 -1
  110. package/dist/core/session-config.js.map +1 -1
  111. package/dist/core/sessions/flat-file-storage.js.map +1 -0
  112. package/dist/core/sessions/lazy-session-manager.js.map +1 -0
  113. package/dist/core/sessions/list-cli-sessions.d.ts +12 -0
  114. package/dist/core/sessions/list-cli-sessions.js +174 -0
  115. package/dist/core/sessions/list-cli-sessions.js.map +1 -0
  116. package/dist/core/skills/built-in/hooman-config/SKILL.md +98 -201
  117. package/dist/core/state/tool-approvals.js +4 -3
  118. package/dist/core/state/tool-approvals.js.map +1 -1
  119. package/dist/core/subagents/index.d.ts +2 -3
  120. package/dist/core/subagents/index.js +2 -3
  121. package/dist/core/subagents/index.js.map +1 -1
  122. package/dist/core/subagents/registry.d.ts +22 -0
  123. package/dist/core/subagents/registry.js +96 -0
  124. package/dist/core/subagents/registry.js.map +1 -0
  125. package/dist/core/subagents/tool.d.ts +6 -14
  126. package/dist/core/subagents/tool.js +73 -53
  127. package/dist/core/subagents/tool.js.map +1 -1
  128. package/dist/core/tools/filesystem.js +2 -9
  129. package/dist/core/tools/filesystem.js.map +1 -1
  130. package/dist/core/tools/shell.js.map +1 -1
  131. package/dist/core/utils/discover-files.d.ts +12 -0
  132. package/dist/core/utils/discover-files.js +47 -0
  133. package/dist/core/utils/discover-files.js.map +1 -0
  134. package/dist/index.d.ts +8 -5
  135. package/dist/index.js +7 -3
  136. package/dist/index.js.map +1 -1
  137. package/package.json +2 -1
  138. package/dist/acp/meta/system-prompt.d.ts +0 -12
  139. package/dist/acp/meta/system-prompt.js +0 -41
  140. package/dist/acp/meta/system-prompt.js.map +0 -1
  141. package/dist/core/context/flat-file-storage.js.map +0 -1
  142. package/dist/core/context/index.d.ts +0 -22
  143. package/dist/core/context/index.js +0 -74
  144. package/dist/core/context/index.js.map +0 -1
  145. package/dist/core/context/lazy-session-manager.js.map +0 -1
  146. package/dist/core/context/model-extractor.js.map +0 -1
  147. package/dist/core/subagents/research.d.ts +0 -16
  148. package/dist/core/subagents/research.js +0 -58
  149. package/dist/core/subagents/research.js.map +0 -1
  150. package/dist/core/subagents/runner.d.ts +0 -37
  151. package/dist/core/subagents/runner.js +0 -273
  152. package/dist/core/subagents/runner.js.map +0 -1
  153. /package/dist/core/{context → memory}/model-extractor.d.ts +0 -0
  154. /package/dist/core/{context → memory}/model-extractor.js +0 -0
  155. /package/dist/core/{context → sessions}/flat-file-storage.d.ts +0 -0
  156. /package/dist/core/{context → sessions}/flat-file-storage.js +0 -0
  157. /package/dist/core/{context → sessions}/lazy-session-manager.d.ts +0 -0
  158. /package/dist/core/{context → sessions}/lazy-session-manager.js +0 -0
@@ -3,14 +3,14 @@ import { useCallback, useEffect, useMemo, useState } from "react";
3
3
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { Box, Text, useApp, useInput } from "ink";
5
5
  import { LlmProvider, } from "../core/config.js";
6
- import { McpOAuthConfigSchema, } from "../core/mcp/oauth/types.js";
6
+ import { McpOAuthConfigSchema } from "../core/mcp/oauth/types.js";
7
7
  import { McpTransportSchema, } from "../core/mcp/types.js";
8
- import { instructionsMdPath, } from "../core/utils/paths.js";
8
+ import { instructionsMdPath } from "../core/utils/paths.js";
9
9
  import { BusyScreen } from "./components/BusyScreen.js";
10
10
  import { MenuScreen } from "./components/MenuScreen.js";
11
11
  import { PromptForm } from "./components/PromptForm.js";
12
12
  import { openFileInEditor } from "./open-in-editor.js";
13
- import { DEFAULT_INSTRUCTIONS, compactJson, folderNameForSkill, paramsPreview, normalizeOptional, noticeColor, parseOptionalBoolean, parseNumber, parseObjectRecord, maskSensitiveParamsForDisplay, parseStringArray, parseStringRecord, transportSummary, truncate, } from "./utils.js";
13
+ import { DEFAULT_INSTRUCTIONS, compactJson, folderNameForSkill, paramsPreview, normalizeOptional, noticeColor, parseOptionalBoolean, parseNumber, maskSensitiveParamsForDisplay, parseStringArray, parseStringRecord, transportSummary, truncate, } from "./utils.js";
14
14
  const PROMPT_LABELS = {
15
15
  behaviour: "Behaviour",
16
16
  communication: "Communication",
@@ -24,36 +24,496 @@ const SEARCH_PROVIDER_LABELS = {
24
24
  serper: "Serper",
25
25
  tavily: "Tavily",
26
26
  };
27
+ const MCP_STDIO_FIELDS = [
28
+ { key: "name", label: "Server name", placeholder: "filesystem" },
29
+ { key: "command", label: "Command", placeholder: "npx" },
30
+ {
31
+ key: "args",
32
+ label: "Arguments",
33
+ placeholder: '["-y", "@modelcontextprotocol/server-filesystem"]',
34
+ note: "Provide a JSON array of strings, or leave as [].",
35
+ },
36
+ {
37
+ key: "env",
38
+ label: "Environment variables",
39
+ placeholder: '{"API_KEY":"..."}',
40
+ note: "Optional JSON object with string values.",
41
+ },
42
+ {
43
+ key: "cwd",
44
+ label: "Working directory",
45
+ placeholder: "/absolute/path",
46
+ note: "Optional working directory for the subprocess.",
47
+ },
48
+ ];
49
+ const MCP_REMOTE_BASE_FIELDS = [
50
+ { key: "name", label: "Server name", placeholder: "my-remote-server" },
51
+ { key: "url", label: "URL", placeholder: "https://example.com/mcp" },
52
+ {
53
+ key: "headers",
54
+ label: "Headers",
55
+ placeholder: '{"Authorization":"Bearer ..."}',
56
+ note: "Optional JSON object with string values.",
57
+ },
58
+ {
59
+ key: "oauthEnabled",
60
+ label: "Enable OAuth? (yes/no)",
61
+ placeholder: "no",
62
+ note: "Choose yes for servers that use OAuth 2.0/2.1 or dynamic client registration.",
63
+ },
64
+ ];
65
+ const MCP_REMOTE_OAUTH_FIELDS = [
66
+ { key: "clientId", label: "OAuth client ID", placeholder: "client-id" },
67
+ {
68
+ key: "clientSecret",
69
+ label: "OAuth client secret",
70
+ placeholder: "secret",
71
+ note: "Stored in mcp.json only if you enter a value.",
72
+ },
73
+ { key: "scopes", label: "OAuth scopes", placeholder: '["read","write"]' },
74
+ {
75
+ key: "audiences",
76
+ label: "OAuth audiences",
77
+ placeholder: '["https://api.example.com"]',
78
+ },
79
+ {
80
+ key: "callbackPort",
81
+ label: "OAuth callback port",
82
+ placeholder: "19876",
83
+ },
84
+ {
85
+ key: "redirectUri",
86
+ label: "OAuth redirect URI",
87
+ placeholder: "http://127.0.0.1:19876/mcp/oauth/callback",
88
+ },
89
+ {
90
+ key: "issuer",
91
+ label: "OAuth issuer",
92
+ placeholder: "https://auth.example.com",
93
+ },
94
+ {
95
+ key: "authorizationUrl",
96
+ label: "OAuth authorization URL override",
97
+ placeholder: "https://auth.example.com/authorize",
98
+ },
99
+ {
100
+ key: "tokenUrl",
101
+ label: "OAuth token URL override",
102
+ placeholder: "https://auth.example.com/token",
103
+ },
104
+ {
105
+ key: "registrationUrl",
106
+ label: "OAuth registration URL override",
107
+ placeholder: "https://auth.example.com/register",
108
+ },
109
+ {
110
+ key: "tokenParamName",
111
+ label: "OAuth token param name",
112
+ placeholder: "access_token",
113
+ },
114
+ ];
115
+ function isTruthyToggle(value) {
116
+ const normalized = value?.trim().toLowerCase();
117
+ return normalized !== undefined
118
+ ? ["y", "yes", "true", "1", "on"].includes(normalized)
119
+ : false;
120
+ }
121
+ function formatDraftFieldValue(field, value) {
122
+ const normalized = value ?? "";
123
+ if (!normalized.trim()) {
124
+ return "not set";
125
+ }
126
+ if (field.key === "clientSecret" ||
127
+ field.key === "env" ||
128
+ field.key === "headers") {
129
+ return paramsPreview(normalized);
130
+ }
131
+ return truncate(normalized, 44);
132
+ }
27
133
  const SUPPORTED_PROVIDER_TYPES = [
28
134
  LlmProvider.Anthropic,
135
+ LlmProvider.Azure,
29
136
  LlmProvider.Bedrock,
30
137
  LlmProvider.Google,
31
138
  LlmProvider.Groq,
139
+ LlmProvider.Minimax,
32
140
  LlmProvider.Moonshot,
33
141
  LlmProvider.Ollama,
34
142
  LlmProvider.OpenAI,
143
+ LlmProvider.OpenRouter,
35
144
  LlmProvider.Xai,
36
145
  ];
37
- function providerParamsTemplate(provider) {
146
+ const DEFAULT_MODEL_BY_PROVIDER = {
147
+ [LlmProvider.Anthropic]: "claude-sonnet-4-6",
148
+ [LlmProvider.Azure]: "gpt-5.4-mini",
149
+ [LlmProvider.Bedrock]: "anthropic.claude-sonnet-4-6",
150
+ [LlmProvider.Google]: "gemini-2.5-flash",
151
+ [LlmProvider.Groq]: "openai/gpt-oss-20b",
152
+ [LlmProvider.Minimax]: "MiniMax-M3",
153
+ [LlmProvider.Moonshot]: "kimi-k2.7-code",
154
+ [LlmProvider.Ollama]: "gemma4:e4b",
155
+ [LlmProvider.OpenAI]: "gpt-5.5",
156
+ [LlmProvider.OpenRouter]: "google/gemma-4-26b-a4b-it:free",
157
+ [LlmProvider.Xai]: "grok-4.3",
158
+ };
159
+ function defaultModelForProviderType(provider) {
160
+ return DEFAULT_MODEL_BY_PROVIDER[provider];
161
+ }
162
+ function providerOptionsTemplate(provider) {
38
163
  switch (provider) {
39
164
  case LlmProvider.Anthropic:
40
165
  return { apiKey: "" };
166
+ case LlmProvider.Azure:
167
+ return {};
41
168
  case LlmProvider.Bedrock:
42
169
  return { region: "us-west-2" };
43
170
  case LlmProvider.Google:
44
171
  return { apiKey: "" };
45
172
  case LlmProvider.Groq:
46
173
  return { apiKey: "" };
174
+ case LlmProvider.Minimax:
175
+ return { apiKey: "" };
47
176
  case LlmProvider.Moonshot:
48
177
  return { apiKey: "" };
49
178
  case LlmProvider.Ollama:
50
179
  return {};
51
180
  case LlmProvider.OpenAI:
52
181
  return { apiKey: "" };
182
+ case LlmProvider.OpenRouter:
183
+ return { apiKey: "" };
53
184
  case LlmProvider.Xai:
54
185
  return { apiKey: "" };
55
186
  }
56
187
  }
188
+ const PROVIDER_FIELD_DEFINITIONS = {
189
+ [LlmProvider.Anthropic]: [
190
+ {
191
+ key: "apiKey",
192
+ label: "API key",
193
+ kind: "string",
194
+ placeholder: "sk-ant-...",
195
+ sensitive: true,
196
+ },
197
+ {
198
+ key: "baseURL",
199
+ label: "Base URL",
200
+ kind: "string",
201
+ placeholder: "https://api.anthropic.com",
202
+ },
203
+ {
204
+ key: "headers",
205
+ label: "Headers",
206
+ kind: "stringRecord",
207
+ placeholder: '{"x-my-header":"value"}',
208
+ },
209
+ {
210
+ key: "thinking",
211
+ label: "Thinking",
212
+ kind: "anthropicThinking",
213
+ placeholder: "adaptive",
214
+ note: 'Allowed: "disabled", "adaptive", or blank to clear.',
215
+ },
216
+ ],
217
+ [LlmProvider.Azure]: [
218
+ {
219
+ key: "resourceName",
220
+ label: "Resource name",
221
+ kind: "string",
222
+ placeholder: "your-resource-name",
223
+ note: "Used to build the Azure OpenAI base URL when `baseURL` is not set.",
224
+ },
225
+ {
226
+ key: "baseURL",
227
+ label: "Base URL",
228
+ kind: "string",
229
+ placeholder: "https://your-resource-name.openai.azure.com/openai",
230
+ note: "Optional override for the Azure OpenAI endpoint prefix. When set, it takes precedence over `resourceName`.",
231
+ },
232
+ {
233
+ key: "apiKey",
234
+ label: "API key",
235
+ kind: "string",
236
+ placeholder: "...",
237
+ sensitive: true,
238
+ },
239
+ {
240
+ key: "headers",
241
+ label: "Headers",
242
+ kind: "stringRecord",
243
+ placeholder: '{"x-my-header":"value"}',
244
+ },
245
+ {
246
+ key: "apiVersion",
247
+ label: "API version",
248
+ kind: "string",
249
+ placeholder: "preview",
250
+ note: "Leave blank to use the AI SDK default API version.",
251
+ },
252
+ {
253
+ key: "useDeploymentBasedUrls",
254
+ label: "Deployment-based URLs",
255
+ kind: "optionalBoolean",
256
+ placeholder: "false",
257
+ note: "Allowed: yes/no/true/false. Leave blank to use the AI SDK default.",
258
+ },
259
+ ],
260
+ [LlmProvider.Bedrock]: [
261
+ {
262
+ key: "region",
263
+ label: "Region",
264
+ kind: "string",
265
+ placeholder: "us-west-2",
266
+ },
267
+ {
268
+ key: "credentials",
269
+ label: "Static credentials",
270
+ kind: "bedrockCredentials",
271
+ note: "Set both access key ID and secret access key together, or leave blank to rely on the AWS default credential chain.",
272
+ },
273
+ {
274
+ key: "sessionToken",
275
+ label: "Session token",
276
+ kind: "string",
277
+ placeholder: "...",
278
+ sensitive: true,
279
+ },
280
+ {
281
+ key: "apiKey",
282
+ label: "API key",
283
+ kind: "string",
284
+ placeholder: "...",
285
+ sensitive: true,
286
+ },
287
+ ],
288
+ [LlmProvider.Google]: [
289
+ {
290
+ key: "apiKey",
291
+ label: "API key",
292
+ kind: "string",
293
+ placeholder: "...",
294
+ sensitive: true,
295
+ },
296
+ ],
297
+ [LlmProvider.Groq]: [
298
+ {
299
+ key: "apiKey",
300
+ label: "API key",
301
+ kind: "string",
302
+ placeholder: "gsk_...",
303
+ sensitive: true,
304
+ },
305
+ {
306
+ key: "baseURL",
307
+ label: "Base URL",
308
+ kind: "string",
309
+ placeholder: "https://api.groq.com/openai/v1",
310
+ },
311
+ {
312
+ key: "headers",
313
+ label: "Headers",
314
+ kind: "stringRecord",
315
+ placeholder: '{"x-my-header":"value"}',
316
+ },
317
+ ],
318
+ [LlmProvider.Minimax]: [
319
+ {
320
+ key: "apiKey",
321
+ label: "API key",
322
+ kind: "string",
323
+ placeholder: "...",
324
+ sensitive: true,
325
+ },
326
+ {
327
+ key: "headers",
328
+ label: "Headers",
329
+ kind: "stringRecord",
330
+ placeholder: '{"x-my-header":"value"}',
331
+ },
332
+ {
333
+ key: "thinking",
334
+ label: "Thinking",
335
+ kind: "anthropicThinking",
336
+ placeholder: "adaptive",
337
+ note: 'Allowed: "disabled", "adaptive", or blank to clear.',
338
+ },
339
+ ],
340
+ [LlmProvider.Moonshot]: [
341
+ {
342
+ key: "apiKey",
343
+ label: "API key",
344
+ kind: "string",
345
+ placeholder: "...",
346
+ sensitive: true,
347
+ },
348
+ {
349
+ key: "baseURL",
350
+ label: "Base URL",
351
+ kind: "string",
352
+ placeholder: "https://api.moonshot.ai/v1",
353
+ note: "Leave blank to use the default Moonshot API base URL.",
354
+ },
355
+ {
356
+ key: "headers",
357
+ label: "Headers",
358
+ kind: "stringRecord",
359
+ placeholder: '{"x-my-header":"value"}',
360
+ },
361
+ ],
362
+ [LlmProvider.Ollama]: [
363
+ {
364
+ key: "baseURL",
365
+ label: "Base URL",
366
+ kind: "string",
367
+ placeholder: "http://127.0.0.1:11434",
368
+ },
369
+ {
370
+ key: "thinking",
371
+ label: "Thinking",
372
+ kind: "ollamaThinking",
373
+ placeholder: "medium",
374
+ note: 'Allowed: yes/no/true/false or "high", "medium", "low". Leave blank to clear.',
375
+ },
376
+ ],
377
+ [LlmProvider.OpenAI]: [
378
+ {
379
+ key: "apiKey",
380
+ label: "API key",
381
+ kind: "string",
382
+ placeholder: "sk-...",
383
+ sensitive: true,
384
+ },
385
+ {
386
+ key: "baseURL",
387
+ label: "Base URL",
388
+ kind: "string",
389
+ placeholder: "https://api.openai.com/v1",
390
+ },
391
+ {
392
+ key: "headers",
393
+ label: "Headers",
394
+ kind: "stringRecord",
395
+ placeholder: '{"x-my-header":"value"}',
396
+ },
397
+ ],
398
+ [LlmProvider.OpenRouter]: [
399
+ {
400
+ key: "apiKey",
401
+ label: "API key",
402
+ kind: "string",
403
+ placeholder: "sk-or-...",
404
+ sensitive: true,
405
+ },
406
+ {
407
+ key: "baseURL",
408
+ label: "Base URL",
409
+ kind: "string",
410
+ placeholder: "https://openrouter.ai/api/v1",
411
+ note: "Leave blank to use the default OpenRouter API base URL.",
412
+ },
413
+ {
414
+ key: "headers",
415
+ label: "Headers",
416
+ kind: "stringRecord",
417
+ placeholder: '{"HTTP-Referer":"https://example.com","X-Title":"Hooman"}',
418
+ note: "Optional extra headers such as attribution metadata for OpenRouter.",
419
+ },
420
+ ],
421
+ [LlmProvider.Xai]: [
422
+ {
423
+ key: "apiKey",
424
+ label: "API key",
425
+ kind: "string",
426
+ placeholder: "...",
427
+ sensitive: true,
428
+ },
429
+ {
430
+ key: "baseURL",
431
+ label: "Base URL",
432
+ kind: "string",
433
+ placeholder: "https://api.x.ai/v1",
434
+ },
435
+ {
436
+ key: "headers",
437
+ label: "Headers",
438
+ kind: "stringRecord",
439
+ placeholder: '{"x-my-header":"value"}',
440
+ },
441
+ ],
442
+ };
443
+ const LLM_FIELD_DEFINITIONS = [
444
+ {
445
+ key: "temperature",
446
+ label: "Temperature",
447
+ kind: "optionalNumber",
448
+ placeholder: "0.7",
449
+ note: "Leave blank to clear.",
450
+ },
451
+ {
452
+ key: "maxTokens",
453
+ label: "Max tokens",
454
+ kind: "optionalInteger",
455
+ placeholder: "4096",
456
+ note: "Leave blank to clear.",
457
+ },
458
+ ];
459
+ function formatTypedFieldValue(definition, value) {
460
+ if (value === undefined) {
461
+ return "not set";
462
+ }
463
+ if (definition.sensitive) {
464
+ return "[REDACTED]";
465
+ }
466
+ if (definition.kind === "bedrockCredentials") {
467
+ return value ? "[REDACTED]" : "not set";
468
+ }
469
+ if (definition.kind === "stringRecord") {
470
+ return paramsPreview(value);
471
+ }
472
+ return truncate(String(value), 44);
473
+ }
474
+ function parseTypedFieldValue(input, definition) {
475
+ switch (definition.kind) {
476
+ case "string":
477
+ return normalizeOptional(input);
478
+ case "stringRecord":
479
+ return parseStringRecord(input, definition.label);
480
+ case "optionalBoolean":
481
+ return parseOptionalBoolean(input, definition.label);
482
+ case "optionalNumber":
483
+ return normalizeOptional(input) === undefined
484
+ ? undefined
485
+ : parseNumber(input, definition.label);
486
+ case "optionalInteger":
487
+ return normalizeOptional(input) === undefined
488
+ ? undefined
489
+ : parseNumber(input, definition.label, {
490
+ integer: true,
491
+ min: 1,
492
+ });
493
+ case "bedrockCredentials":
494
+ return undefined;
495
+ case "anthropicThinking": {
496
+ const value = normalizeOptional(input);
497
+ if (value === undefined) {
498
+ return undefined;
499
+ }
500
+ if (value === "disabled" || value === "adaptive") {
501
+ return value;
502
+ }
503
+ throw new Error(`${definition.label} must be "disabled" or "adaptive".`);
504
+ }
505
+ case "ollamaThinking": {
506
+ const value = normalizeOptional(input);
507
+ if (value === undefined) {
508
+ return undefined;
509
+ }
510
+ if (value === "high" || value === "medium" || value === "low") {
511
+ return value;
512
+ }
513
+ return parseOptionalBoolean(value, definition.label);
514
+ }
515
+ }
516
+ }
57
517
  /** On/off display for tool rows (`Tool • Yes` / `Tool • No`). */
58
518
  const yesNo = (on) => (on ? "Yes" : "No");
59
519
  export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, }) {
@@ -63,6 +523,7 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
63
523
  const [notice, setNotice] = useState(null);
64
524
  const [busyMessage, setBusyMessage] = useState(null);
65
525
  const [revision, setRevision] = useState(0);
526
+ const [mcpDraft, setMcpDraft] = useState(null);
66
527
  const [installedSkills, setInstalledSkills] = useState([]);
67
528
  const [searchResults, setSearchResults] = useState([]);
68
529
  const [mcpAuthStatuses, setMcpAuthStatuses] = useState({});
@@ -138,6 +599,12 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
138
599
  setPrompt(null);
139
600
  return;
140
601
  }
602
+ if (screen.kind === "mcp-stdio-edit" ||
603
+ screen.kind === "mcp-remote-edit") {
604
+ setMcpDraft(null);
605
+ setScreen({ kind: "mcp" });
606
+ return;
607
+ }
141
608
  if (screen.kind === "mcp-delete-confirm") {
142
609
  setScreen({ kind: "mcp" });
143
610
  return;
@@ -154,9 +621,21 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
154
621
  setScreen({ kind: "config-provider-edit", name: screen.name });
155
622
  return;
156
623
  }
624
+ if (screen.kind === "config-provider-anthropic-thinking" ||
625
+ screen.kind === "config-provider-ollama-thinking") {
626
+ setScreen({ kind: "config-provider-edit", name: screen.name });
627
+ return;
628
+ }
629
+ if (screen.kind === "config-provider-add-type") {
630
+ setScreen({ kind: "config-providers" });
631
+ return;
632
+ }
157
633
  if (screen.kind !== "home") {
158
634
  setScreen({ kind: "home" });
635
+ return;
159
636
  }
637
+ onExit();
638
+ exit();
160
639
  }, { isActive: true });
161
640
  const setSuccess = useCallback((text) => {
162
641
  setNotice({ kind: "success", text });
@@ -180,21 +659,33 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
180
659
  refresh();
181
660
  setSuccess(message);
182
661
  }, [config, refresh, setSuccess]);
183
- const patchLlm = useCallback((name, patch) => config.llms.map((m) => m.name === name ? { ...m, options: { ...m.options, ...patch } } : m), [config]);
662
+ const patchLlm = useCallback((name, patch) => config.llms.map((m) => m.name === name
663
+ ? {
664
+ ...m,
665
+ options: {
666
+ ...m.options,
667
+ ...patch,
668
+ },
669
+ }
670
+ : m), [config]);
184
671
  const renameLlm = useCallback((oldName, newName) => config.llms.map((m) => m.name === oldName ? { ...m, name: newName } : m), [config]);
185
672
  const setDefaultLlm = useCallback((name) => config.llms.map((m) => ({ ...m, default: m.name === name })), [config]);
186
- const addLlm = useCallback((name) => [
187
- ...config.llms,
188
- {
189
- name,
190
- options: {
191
- provider: config.providers[0]?.name ?? LlmProvider.Ollama,
192
- model: "gemma4:e4b",
193
- params: {},
673
+ const addLlm = useCallback((name) => {
674
+ const providerName = config.providers[0]?.name ?? "Ollama";
675
+ const providerType = config.providers.find((provider) => provider.name === providerName)
676
+ ?.provider ?? LlmProvider.Ollama;
677
+ return [
678
+ ...config.llms,
679
+ {
680
+ name,
681
+ provider: providerName,
682
+ options: {
683
+ model: defaultModelForProviderType(providerType),
684
+ },
685
+ default: false,
194
686
  },
195
- default: false,
196
- },
197
- ], [config]);
687
+ ];
688
+ }, [config]);
198
689
  const removeLlm = useCallback((name) => config.llms.filter((m) => m.name !== name), [config]);
199
690
  const patchProvider = useCallback((name, patch) => config.providers.map((provider) => provider.name === name
200
691
  ? {
@@ -202,29 +693,25 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
202
693
  options: {
203
694
  ...provider.options,
204
695
  ...patch,
205
- params: patch.params ?? provider.options.params,
206
696
  },
207
697
  }
208
698
  : provider), [config]);
209
699
  const renameProvider = useCallback((oldName, newName) => ({
210
700
  providers: config.providers.map((provider) => provider.name === oldName ? { ...provider, name: newName } : provider),
211
- llms: config.llms.map((llm) => llm.options.provider === oldName
701
+ llms: config.llms.map((llm) => llm.provider === oldName
212
702
  ? {
213
703
  ...llm,
214
- options: {
215
- ...llm.options,
216
- provider: newName,
217
- },
704
+ provider: newName,
218
705
  }
219
706
  : llm),
220
707
  }), [config]);
221
- const addProvider = useCallback((name) => [
708
+ const addProvider = useCallback((name, provider) => [
222
709
  ...config.providers,
223
710
  {
224
711
  name,
712
+ provider,
225
713
  options: {
226
- provider: LlmProvider.Ollama,
227
- params: providerParamsTemplate(LlmProvider.Ollama),
714
+ ...providerOptionsTemplate(provider),
228
715
  },
229
716
  },
230
717
  ], [config]);
@@ -246,307 +733,173 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
246
733
  });
247
734
  }
248
735
  }, [prompt]);
249
- const promptForStdio = useCallback((name, initial) => {
250
- const mode = initial ? "Edit" : "Add";
251
- promptValue({
252
- title: `${mode} stdio server`,
253
- label: "Command",
254
- initialValue: initial?.command ?? "",
255
- placeholder: "npx",
256
- onSubmit: async (commandValue) => {
257
- const command = commandValue.trim();
258
- if (!command) {
259
- throw new Error("Command is required.");
260
- }
261
- promptValue({
262
- title: `${mode} stdio server`,
263
- label: "Arguments",
264
- note: 'Example: ["-y", "@modelcontextprotocol/server-filesystem"]',
265
- initialValue: compactJson(initial?.args ?? []),
266
- placeholder: "[]",
267
- onSubmit: async (argsValue) => {
268
- const args = parseStringArray(argsValue, "Arguments");
269
- promptValue({
270
- title: `${mode} stdio server`,
271
- label: "Environment variables (optional)",
272
- initialValue: initial?.env ? compactJson(initial.env) : "",
273
- placeholder: '{"API_KEY":"..."}',
274
- onSubmit: async (envValue) => {
275
- const env = parseStringRecord(envValue, "Environment variables");
276
- promptValue({
277
- title: `${mode} stdio server`,
278
- label: "Working directory (optional)",
279
- initialValue: initial?.cwd ?? "",
280
- placeholder: "/absolute/path",
281
- onSubmit: async (cwdValue) => {
282
- const transport = McpTransportSchema.parse({
283
- type: "stdio",
284
- command,
285
- ...(args.length > 0 ? { args } : {}),
286
- ...(env && Object.keys(env).length > 0 ? { env } : {}),
287
- ...(normalizeOptional(cwdValue)
288
- ? { cwd: normalizeOptional(cwdValue) }
289
- : {}),
290
- });
291
- if (initial) {
292
- mcpConfig.update(name, transport);
293
- setSuccess(`Updated MCP server "${name}".`);
294
- }
295
- else {
296
- mcpConfig.add(name, transport);
297
- setSuccess(`Added MCP server "${name}".`);
298
- }
299
- setPrompt(null);
300
- setScreen({ kind: "mcp" });
301
- refresh();
302
- },
303
- });
304
- },
305
- });
306
- },
307
- });
308
- },
736
+ const persistMcpTransport = useCallback((currentName, nextName, transport) => {
737
+ if (!nextName) {
738
+ throw new Error("Server name is required.");
739
+ }
740
+ if (currentName && currentName !== nextName && mcpConfig.get(nextName)) {
741
+ throw new Error(`MCP server "${nextName}" already exists.`);
742
+ }
743
+ if (!currentName) {
744
+ mcpConfig.add(nextName, transport);
745
+ setSuccess(`Added MCP server "${nextName}".`);
746
+ }
747
+ else if (currentName === nextName) {
748
+ mcpConfig.update(currentName, transport);
749
+ setSuccess(`Updated MCP server "${currentName}".`);
750
+ }
751
+ else {
752
+ mcpConfig.add(nextName, transport);
753
+ mcpConfig.remove(currentName);
754
+ setSuccess(`Renamed MCP server "${currentName}" to "${nextName}".`);
755
+ }
756
+ setMcpDraft(null);
757
+ setScreen({ kind: "mcp" });
758
+ refresh();
759
+ }, [mcpConfig, refresh, setSuccess]);
760
+ const openMcpStdioEditor = useCallback((currentName, initial) => {
761
+ setMcpDraft({
762
+ name: currentName ?? "",
763
+ command: initial?.command ?? "",
764
+ args: compactJson(initial?.args ?? []),
765
+ env: initial?.env ? compactJson(initial.env) : "",
766
+ cwd: initial?.cwd ?? "",
309
767
  });
310
- }, [mcpConfig, promptValue, refresh, setSuccess]);
311
- const promptForRemote = useCallback((name, type, initial) => {
312
- const mode = initial ? "Edit" : "Add";
313
- const persistRemote = (url, headers, oauth) => {
314
- const transport = McpTransportSchema.parse({
315
- type,
316
- url,
317
- ...(headers && Object.keys(headers).length > 0 ? { headers } : {}),
318
- ...(oauth ? { oauth } : {}),
319
- });
320
- if (initial) {
321
- mcpConfig.update(name, transport);
322
- setSuccess(`Updated MCP server "${name}".`);
323
- }
324
- else {
325
- mcpConfig.add(name, transport);
326
- setSuccess(`Added MCP server "${name}".`);
327
- }
328
- setPrompt(null);
329
- setScreen({ kind: "mcp" });
330
- refresh();
331
- };
332
- const promptForOAuthDetails = (url, headers, oauthEnabled) => {
333
- if (!oauthEnabled) {
334
- persistRemote(url, headers, undefined);
335
- return;
336
- }
337
- promptValue({
338
- title: `${mode} ${type} server`,
339
- label: "OAuth client ID (optional)",
340
- initialValue: initial?.oauth?.clientId ?? "",
341
- placeholder: "client-id",
342
- onSubmit: async (clientIdValue) => {
343
- promptValue({
344
- title: `${mode} ${type} server`,
345
- label: "OAuth client secret (optional)",
346
- initialValue: initial?.oauth?.clientSecret ?? "",
347
- placeholder: "secret",
348
- note: "Stored in mcp.json only if you enter a value.",
349
- onSubmit: async (clientSecretValue) => {
350
- promptValue({
351
- title: `${mode} ${type} server`,
352
- label: "OAuth scopes (optional)",
353
- initialValue: initial?.oauth?.scopes
354
- ? compactJson(initial.oauth.scopes)
355
- : "",
356
- placeholder: '["read","write"]',
357
- onSubmit: async (scopesValue) => {
358
- promptValue({
359
- title: `${mode} ${type} server`,
360
- label: "OAuth audiences (optional)",
361
- initialValue: initial?.oauth?.audiences
362
- ? compactJson(initial.oauth.audiences)
363
- : "",
364
- placeholder: '["https://api.example.com"]',
365
- onSubmit: async (audiencesValue) => {
366
- promptValue({
367
- title: `${mode} ${type} server`,
368
- label: "OAuth callback port (optional)",
369
- initialValue: initial?.oauth?.callbackPort !== undefined
370
- ? String(initial.oauth.callbackPort)
371
- : "",
372
- placeholder: "19876",
373
- onSubmit: async (callbackPortValue) => {
374
- promptValue({
375
- title: `${mode} ${type} server`,
376
- label: "OAuth redirect URI (optional)",
377
- initialValue: initial?.oauth?.redirectUri ?? "",
378
- placeholder: "http://127.0.0.1:19876/mcp/oauth/callback",
379
- onSubmit: async (redirectUriValue) => {
380
- promptValue({
381
- title: `${mode} ${type} server`,
382
- label: "OAuth issuer (optional)",
383
- initialValue: initial?.oauth?.issuer ?? "",
384
- placeholder: "https://auth.example.com",
385
- onSubmit: async (issuerValue) => {
386
- promptValue({
387
- title: `${mode} ${type} server`,
388
- label: "OAuth authorization URL override (optional)",
389
- initialValue: initial?.oauth?.authorizationUrl ?? "",
390
- placeholder: "https://auth.example.com/authorize",
391
- onSubmit: async (authorizationUrlValue) => {
392
- promptValue({
393
- title: `${mode} ${type} server`,
394
- label: "OAuth token URL override (optional)",
395
- initialValue: initial?.oauth?.tokenUrl ?? "",
396
- placeholder: "https://auth.example.com/token",
397
- onSubmit: async (tokenUrlValue) => {
398
- promptValue({
399
- title: `${mode} ${type} server`,
400
- label: "OAuth registration URL override (optional)",
401
- initialValue: initial?.oauth
402
- ?.registrationUrl ?? "",
403
- placeholder: "https://auth.example.com/register",
404
- onSubmit: async (registrationUrlValue) => {
405
- promptValue({
406
- title: `${mode} ${type} server`,
407
- label: "OAuth token param name (optional)",
408
- initialValue: initial?.oauth
409
- ?.tokenParamName ?? "",
410
- placeholder: "access_token",
411
- onSubmit: async (tokenParamNameValue) => {
412
- const scopes = parseStringArray(scopesValue, "OAuth scopes");
413
- const audiences = parseStringArray(audiencesValue, "OAuth audiences");
414
- const callbackPort = normalizeOptional(callbackPortValue) !== undefined
415
- ? parseNumber(callbackPortValue, "OAuth callback port", {
416
- integer: true,
417
- min: 1,
418
- max: 65535,
419
- })
420
- : undefined;
421
- const oauth = McpOAuthConfigSchema.parse({
422
- enabled: true,
423
- ...(normalizeOptional(clientIdValue)
424
- ? {
425
- clientId: normalizeOptional(clientIdValue),
426
- }
427
- : {}),
428
- ...(normalizeOptional(clientSecretValue)
429
- ? {
430
- clientSecret: normalizeOptional(clientSecretValue),
431
- }
432
- : {}),
433
- ...(scopes.length > 0
434
- ? { scopes }
435
- : {}),
436
- ...(audiences.length >
437
- 0
438
- ? { audiences }
439
- : {}),
440
- ...(callbackPort !==
441
- undefined
442
- ? { callbackPort }
443
- : {}),
444
- ...(normalizeOptional(redirectUriValue)
445
- ? {
446
- redirectUri: normalizeOptional(redirectUriValue),
447
- }
448
- : {}),
449
- ...(normalizeOptional(issuerValue)
450
- ? {
451
- issuer: normalizeOptional(issuerValue),
452
- }
453
- : {}),
454
- ...(normalizeOptional(authorizationUrlValue)
455
- ? {
456
- authorizationUrl: normalizeOptional(authorizationUrlValue),
457
- }
458
- : {}),
459
- ...(normalizeOptional(tokenUrlValue)
460
- ? {
461
- tokenUrl: normalizeOptional(tokenUrlValue),
462
- }
463
- : {}),
464
- ...(normalizeOptional(registrationUrlValue)
465
- ? {
466
- registrationUrl: normalizeOptional(registrationUrlValue),
467
- }
468
- : {}),
469
- ...(normalizeOptional(tokenParamNameValue)
470
- ? {
471
- tokenParamName: normalizeOptional(tokenParamNameValue),
472
- }
473
- : {}),
474
- });
475
- persistRemote(url, headers, oauth);
476
- },
477
- });
478
- },
479
- });
480
- },
481
- });
482
- },
483
- });
484
- },
485
- });
486
- },
487
- });
488
- },
489
- });
490
- },
491
- });
492
- },
493
- });
494
- },
495
- });
496
- },
497
- });
498
- };
499
- promptValue({
500
- title: `${mode} ${type} server`,
501
- label: "URL",
502
- initialValue: initial?.url ?? "",
503
- placeholder: "https://example.com/mcp",
504
- onSubmit: async (urlValue) => {
505
- const url = urlValue.trim();
506
- if (!url) {
507
- throw new Error("URL is required.");
508
- }
509
- promptValue({
510
- title: `${mode} ${type} server`,
511
- label: "Headers (optional)",
512
- initialValue: initial?.headers ? compactJson(initial.headers) : "",
513
- placeholder: '{"Authorization":"Bearer ..."}',
514
- onSubmit: async (headersValue) => {
515
- const headers = parseStringRecord(headersValue, "Headers");
516
- promptValue({
517
- title: `${mode} ${type} server`,
518
- label: "Enable OAuth? (yes/no)",
519
- initialValue: initial?.oauth ? "yes" : "no",
520
- placeholder: "no",
521
- note: "Choose yes for servers that use OAuth 2.0/2.1 or dynamic client registration.",
522
- onSubmit: async (oauthEnabledValue) => {
523
- const oauthEnabled = parseOptionalBoolean(oauthEnabledValue, "Enable OAuth");
524
- promptForOAuthDetails(url, headers, oauthEnabled);
525
- },
526
- });
527
- },
528
- });
529
- },
768
+ setScreen({ kind: "mcp-stdio-edit", originalName: currentName });
769
+ }, []);
770
+ const openMcpRemoteEditor = useCallback((type, currentName, initial) => {
771
+ setMcpDraft({
772
+ name: currentName ?? "",
773
+ url: initial?.url ?? "",
774
+ headers: initial?.headers ? compactJson(initial.headers) : "",
775
+ oauthEnabled: initial?.oauth ? "yes" : "no",
776
+ clientId: initial?.oauth?.clientId ?? "",
777
+ clientSecret: initial?.oauth?.clientSecret ?? "",
778
+ scopes: initial?.oauth?.scopes ? compactJson(initial.oauth.scopes) : "",
779
+ audiences: initial?.oauth?.audiences
780
+ ? compactJson(initial.oauth.audiences)
781
+ : "",
782
+ callbackPort: initial?.oauth?.callbackPort !== undefined
783
+ ? String(initial.oauth.callbackPort)
784
+ : "",
785
+ redirectUri: initial?.oauth?.redirectUri ?? "",
786
+ issuer: initial?.oauth?.issuer ?? "",
787
+ authorizationUrl: initial?.oauth?.authorizationUrl ?? "",
788
+ tokenUrl: initial?.oauth?.tokenUrl ?? "",
789
+ registrationUrl: initial?.oauth?.registrationUrl ?? "",
790
+ tokenParamName: initial?.oauth?.tokenParamName ?? "",
791
+ });
792
+ setScreen({
793
+ kind: "mcp-remote-edit",
794
+ transportType: type,
795
+ originalName: currentName,
530
796
  });
531
- }, [mcpConfig, promptValue, refresh, setSuccess]);
532
- const promptForServerName = useCallback((type) => {
797
+ }, []);
798
+ const updateMcpDraftField = useCallback((key, value) => {
799
+ setMcpDraft((current) => current ? { ...current, [key]: value } : current);
800
+ }, []);
801
+ const editMcpDraftField = useCallback((field, initialValue) => {
533
802
  promptValue({
534
- title: `Add ${type} server`,
535
- label: "Server name",
536
- placeholder: "filesystem",
537
- onSubmit: async (nameValue) => {
538
- const name = nameValue.trim();
539
- if (!name) {
540
- throw new Error("Server name is required.");
541
- }
542
- if (type === "stdio") {
543
- promptForStdio(name);
544
- return;
545
- }
546
- promptForRemote(name, type);
803
+ title: `Update ${field.label}`,
804
+ label: field.label,
805
+ initialValue,
806
+ placeholder: field.placeholder,
807
+ note: field.note,
808
+ onSubmit: async (value) => {
809
+ updateMcpDraftField(field.key, value);
810
+ setPrompt(null);
547
811
  },
548
812
  });
549
- }, [promptForRemote, promptForStdio, promptValue]);
813
+ }, [promptValue, updateMcpDraftField]);
814
+ const saveMcpStdioDraft = useCallback((originalName) => {
815
+ const values = mcpDraft ?? {};
816
+ const name = (values.name ?? "").trim();
817
+ const command = (values.command ?? "").trim();
818
+ if (!name) {
819
+ throw new Error("Server name is required.");
820
+ }
821
+ if (!command) {
822
+ throw new Error("Command is required.");
823
+ }
824
+ const args = parseStringArray(values.args ?? "", "Arguments");
825
+ const env = parseStringRecord(values.env ?? "", "Environment variables");
826
+ const cwd = normalizeOptional(values.cwd ?? "");
827
+ const transport = McpTransportSchema.parse({
828
+ type: "stdio",
829
+ command,
830
+ ...(args.length > 0 ? { args } : {}),
831
+ ...(env && Object.keys(env).length > 0 ? { env } : {}),
832
+ ...(cwd ? { cwd } : {}),
833
+ });
834
+ persistMcpTransport(originalName, name, transport);
835
+ }, [mcpDraft, persistMcpTransport]);
836
+ const saveMcpRemoteDraft = useCallback((transportType, originalName) => {
837
+ const values = mcpDraft ?? {};
838
+ const name = (values.name ?? "").trim();
839
+ const url = (values.url ?? "").trim();
840
+ if (!name) {
841
+ throw new Error("Server name is required.");
842
+ }
843
+ if (!url) {
844
+ throw new Error("URL is required.");
845
+ }
846
+ const headers = parseStringRecord(values.headers ?? "", "Headers");
847
+ const oauthEnabled = parseOptionalBoolean(values.oauthEnabled ?? "", "Enable OAuth");
848
+ const scopes = parseStringArray(values.scopes ?? "", "OAuth scopes");
849
+ const audiences = parseStringArray(values.audiences ?? "", "OAuth audiences");
850
+ const callbackPort = normalizeOptional(values.callbackPort ?? "") !== undefined
851
+ ? parseNumber(values.callbackPort ?? "", "OAuth callback port", {
852
+ integer: true,
853
+ min: 1,
854
+ max: 65535,
855
+ })
856
+ : undefined;
857
+ const oauth = oauthEnabled
858
+ ? McpOAuthConfigSchema.parse({
859
+ enabled: true,
860
+ ...(normalizeOptional(values.clientId ?? "")
861
+ ? { clientId: normalizeOptional(values.clientId ?? "") }
862
+ : {}),
863
+ ...(normalizeOptional(values.clientSecret ?? "")
864
+ ? { clientSecret: normalizeOptional(values.clientSecret ?? "") }
865
+ : {}),
866
+ ...(scopes.length > 0 ? { scopes } : {}),
867
+ ...(audiences.length > 0 ? { audiences } : {}),
868
+ ...(callbackPort !== undefined ? { callbackPort } : {}),
869
+ ...(normalizeOptional(values.redirectUri ?? "")
870
+ ? { redirectUri: normalizeOptional(values.redirectUri ?? "") }
871
+ : {}),
872
+ ...(normalizeOptional(values.issuer ?? "")
873
+ ? { issuer: normalizeOptional(values.issuer ?? "") }
874
+ : {}),
875
+ ...(normalizeOptional(values.authorizationUrl ?? "")
876
+ ? {
877
+ authorizationUrl: normalizeOptional(values.authorizationUrl ?? ""),
878
+ }
879
+ : {}),
880
+ ...(normalizeOptional(values.tokenUrl ?? "")
881
+ ? { tokenUrl: normalizeOptional(values.tokenUrl ?? "") }
882
+ : {}),
883
+ ...(normalizeOptional(values.registrationUrl ?? "")
884
+ ? {
885
+ registrationUrl: normalizeOptional(values.registrationUrl ?? ""),
886
+ }
887
+ : {}),
888
+ ...(normalizeOptional(values.tokenParamName ?? "")
889
+ ? {
890
+ tokenParamName: normalizeOptional(values.tokenParamName ?? ""),
891
+ }
892
+ : {}),
893
+ })
894
+ : undefined;
895
+ const transport = McpTransportSchema.parse({
896
+ type: transportType,
897
+ url,
898
+ ...(headers && Object.keys(headers).length > 0 ? { headers } : {}),
899
+ ...(oauth ? { oauth } : {}),
900
+ });
901
+ persistMcpTransport(originalName, name, transport);
902
+ }, [mcpDraft, persistMcpTransport]);
550
903
  const llmSummary = useCallback((entry) => {
551
904
  const compactModelId = (model) => {
552
905
  if (model.length <= 23) {
@@ -556,16 +909,17 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
556
909
  };
557
910
  const resolved = config.resolveLlm(entry.name);
558
911
  if (!resolved) {
559
- return `${entry.options.provider}/${compactModelId(entry.options.model)}`;
912
+ return `${entry.provider}/${compactModelId(entry.options.model)}`;
560
913
  }
561
- return `${entry.options.provider} -> ${resolved.options.provider}/${compactModelId(resolved.options.model)}`;
914
+ return `${entry.provider} -> ${resolved.provider}/${compactModelId(resolved.llmOptions.model)}`;
562
915
  }, [config]);
563
- const providerUsageCount = useCallback((name) => config.llms.filter((llm) => llm.options.provider === name).length, [config]);
916
+ const providerUsageCount = useCallback((name) => config.llms.filter((llm) => llm.provider === name).length, [config]);
564
917
  const renderHome = () => {
918
+ const defaultLlm = config.llms.find((m) => m.default) ?? config.llms[0];
565
919
  const items = [
566
920
  {
567
- label: "Inference",
568
- value: () => setScreen({ kind: "config" }),
921
+ label: "General",
922
+ value: () => setScreen({ kind: "config-general" }),
569
923
  },
570
924
  {
571
925
  label: "Instructions • edit instructions.md",
@@ -590,6 +944,10 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
590
944
  }
591
945
  },
592
946
  },
947
+ {
948
+ label: defaultLlm ? `Models • ${defaultLlm.name}` : "Models",
949
+ value: () => setScreen({ kind: "config" }),
950
+ },
593
951
  {
594
952
  label: `MCP servers • ${mcpServers.length} configured`,
595
953
  value: () => setScreen({ kind: "mcp" }),
@@ -599,16 +957,16 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
599
957
  value: () => setScreen({ kind: "skills" }),
600
958
  },
601
959
  {
602
- label: "Exit",
960
+ label: "Back",
603
961
  value: () => {
604
962
  onExit();
605
963
  exit();
606
964
  },
607
965
  },
608
966
  ];
609
- return (_jsx(MenuScreen, { title: configData.name, description: `inference: ${llmSummary(config.llms.find((m) => m.default) ?? config.llms[0])}`, items: items, footerHint: "enter: select | ctrl+c: exit" }));
967
+ 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" }));
610
968
  };
611
- const renderConfigMenu = () => {
969
+ const renderGeneralMenu = () => {
612
970
  const enabledPrompts = Object.values(configData.prompts).filter(Boolean).length;
613
971
  const totalPrompts = Object.keys(configData.prompts).length;
614
972
  const items = [
@@ -628,17 +986,6 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
628
986
  },
629
987
  }),
630
988
  },
631
- {
632
- label: (() => {
633
- const def = config.llms.find((m) => m.default) ?? config.llms[0];
634
- return `LLMs • ${config.llms.length} configured (default: ${def.name})`;
635
- })(),
636
- value: () => setScreen({ kind: "config-llms" }),
637
- },
638
- {
639
- label: `Providers • ${config.providers.length} configured`,
640
- value: () => setScreen({ kind: "config-providers" }),
641
- },
642
989
  {
643
990
  label: `Prompts • ${enabledPrompts}/${totalPrompts} enabled`,
644
991
  value: () => setScreen({ kind: "config-prompts" }),
@@ -694,7 +1041,25 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
694
1041
  value: () => setScreen({ kind: "home" }),
695
1042
  },
696
1043
  ];
697
- return (_jsx(MenuScreen, { title: "Configuration", description: "Edit the same values loaded from ~/.hooman/config.json.", items: items }));
1044
+ return (_jsx(MenuScreen, { title: "General", description: "Manage app-wide settings loaded from ~/.hooman/config.json.", items: items }));
1045
+ };
1046
+ const renderConfigMenu = () => {
1047
+ const defaultLlm = config.llms.find((m) => m.default) ?? config.llms[0];
1048
+ const items = [
1049
+ {
1050
+ label: `LLMs • ${config.llms.length} configured (default: ${defaultLlm.name})`,
1051
+ value: () => setScreen({ kind: "config-llms" }),
1052
+ },
1053
+ {
1054
+ label: `Providers • ${config.providers.length} configured`,
1055
+ value: () => setScreen({ kind: "config-providers" }),
1056
+ },
1057
+ {
1058
+ label: "Back",
1059
+ value: () => setScreen({ kind: "home" }),
1060
+ },
1061
+ ];
1062
+ return (_jsx(MenuScreen, { title: "Models", description: "Manage configured providers and LLMs from ~/.hooman/config.json.", items: items }));
698
1063
  };
699
1064
  const renderToolsConfigMenu = () => {
700
1065
  const items = [
@@ -773,23 +1138,23 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
773
1138
  },
774
1139
  },
775
1140
  {
776
- label: `Subagents tool • ${yesNo(configData.tools.agents.enabled)}`,
1141
+ label: `Subagents • ${yesNo(configData.tools.subagents.enabled)}`,
777
1142
  value: () => {
778
1143
  updateConfig({
779
1144
  tools: {
780
1145
  ...config.tools,
781
- agents: {
782
- ...config.tools.agents,
783
- enabled: !configData.tools.agents.enabled,
1146
+ subagents: {
1147
+ ...config.tools.subagents,
1148
+ enabled: !configData.tools.subagents.enabled,
784
1149
  },
785
1150
  },
786
- }, `Subagents tool ${configData.tools.agents.enabled ? "disabled" : "enabled"}.`);
1151
+ }, `Subagents ${configData.tools.subagents.enabled ? "disabled" : "enabled"}.`);
787
1152
  setScreen({ kind: "config-tools" });
788
1153
  },
789
1154
  },
790
1155
  {
791
1156
  label: "Back",
792
- value: () => setScreen({ kind: "config" }),
1157
+ value: () => setScreen({ kind: "config-general" }),
793
1158
  },
794
1159
  ];
795
1160
  return (_jsx(MenuScreen, { title: "Tools", description: "Enable, disable, and configure built-in tools.", items: items }));
@@ -797,7 +1162,7 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
797
1162
  const renderProvidersMenu = () => {
798
1163
  const providerItems = config.providers.map((provider) => ({
799
1164
  key: `provider:${provider.name}`,
800
- label: `${provider.name} • ${provider.options.provider} • ${providerUsageCount(provider.name)} model(s)`,
1165
+ label: `${provider.name} • ${provider.provider} • ${providerUsageCount(provider.name)} model(s)`,
801
1166
  boldSubstring: provider.name,
802
1167
  value: () => setScreen({ kind: "config-provider-edit", name: provider.name }),
803
1168
  }));
@@ -816,9 +1181,8 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
816
1181
  if (config.providers.some((provider) => provider.name === name)) {
817
1182
  throw new Error(`A provider named "${name}" already exists.`);
818
1183
  }
819
- updateConfig({ providers: addProvider(name) }, `Added provider "${name}" with an Ollama scaffold.`);
820
1184
  setPrompt(null);
821
- setScreen({ kind: "config-provider-edit", name });
1185
+ setScreen({ kind: "config-provider-add-type", name });
822
1186
  },
823
1187
  }),
824
1188
  },
@@ -840,6 +1204,8 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
840
1204
  return null;
841
1205
  }
842
1206
  const usageCount = providerUsageCount(entry.name);
1207
+ const providerFields = PROVIDER_FIELD_DEFINITIONS[entry.provider];
1208
+ const providerOptions = entry.options;
843
1209
  const items = [
844
1210
  {
845
1211
  label: `Name • ${entry.name}`,
@@ -866,23 +1232,90 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
866
1232
  }),
867
1233
  },
868
1234
  {
869
- label: `Type • ${entry.options.provider}`,
1235
+ label: `Type • ${entry.provider}`,
870
1236
  value: () => setScreen({ kind: "config-provider-type", name: entry.name }),
871
1237
  },
872
- {
873
- label: `Params • ${paramsPreview(entry.options.params)}`,
874
- value: () => promptValue({
875
- title: "Update provider params",
876
- label: "Parameters",
877
- initialValue: compactJson(entry.options.params),
878
- placeholder: '{"apiKey":"..."}',
879
- onSubmit: async (value) => {
880
- const params = parseObjectRecord(value, "Provider params");
881
- updateConfig({ providers: patchProvider(entry.name, { params }) }, "Updated provider params.");
882
- setPrompt(null);
883
- },
884
- }),
885
- },
1238
+ ...providerFields.map((definition) => ({
1239
+ key: `provider-field:${entry.name}:${definition.key}`,
1240
+ label: `${definition.label} • ${formatTypedFieldValue(definition, definition.kind === "bedrockCredentials"
1241
+ ? providerOptions.accessKeyId && providerOptions.secretAccessKey
1242
+ : providerOptions[definition.key])}`,
1243
+ value: () => {
1244
+ if (definition.kind === "anthropicThinking") {
1245
+ setScreen({
1246
+ kind: "config-provider-anthropic-thinking",
1247
+ name: entry.name,
1248
+ });
1249
+ return;
1250
+ }
1251
+ if (definition.kind === "ollamaThinking") {
1252
+ setScreen({
1253
+ kind: "config-provider-ollama-thinking",
1254
+ name: entry.name,
1255
+ });
1256
+ return;
1257
+ }
1258
+ if (definition.kind === "bedrockCredentials") {
1259
+ promptValue({
1260
+ title: "Update static credentials",
1261
+ label: "Access key ID",
1262
+ initialValue: typeof providerOptions.accessKeyId === "string"
1263
+ ? providerOptions.accessKeyId
1264
+ : "",
1265
+ placeholder: "AKIA...",
1266
+ note: definition.note,
1267
+ onSubmit: async (accessKeyIdValue) => {
1268
+ promptValue({
1269
+ title: "Update static credentials",
1270
+ label: "Secret access key",
1271
+ initialValue: typeof providerOptions.secretAccessKey === "string"
1272
+ ? providerOptions.secretAccessKey
1273
+ : "",
1274
+ placeholder: "...",
1275
+ onSubmit: async (secretAccessKeyValue) => {
1276
+ const accessKeyId = normalizeOptional(accessKeyIdValue);
1277
+ const secretAccessKey = normalizeOptional(secretAccessKeyValue);
1278
+ if ((accessKeyId === undefined) !==
1279
+ (secretAccessKey === undefined)) {
1280
+ throw new Error("Access key ID and secret access key must be provided together.");
1281
+ }
1282
+ updateConfig({
1283
+ providers: patchProvider(entry.name, {
1284
+ accessKeyId,
1285
+ secretAccessKey,
1286
+ }),
1287
+ }, `Updated static credentials for "${entry.name}".`);
1288
+ setPrompt(null);
1289
+ },
1290
+ });
1291
+ },
1292
+ });
1293
+ return;
1294
+ }
1295
+ promptValue({
1296
+ title: `Update ${definition.label}`,
1297
+ label: definition.label,
1298
+ initialValue: definition.kind === "stringRecord"
1299
+ ? providerOptions[definition.key]
1300
+ ? compactJson(providerOptions[definition.key])
1301
+ : ""
1302
+ : providerOptions[definition.key] !== undefined
1303
+ ? String(providerOptions[definition.key])
1304
+ : "",
1305
+ placeholder: definition.placeholder,
1306
+ note: definition.note,
1307
+ onSubmit: async (value) => {
1308
+ const nextValue = parseTypedFieldValue(value, definition);
1309
+ updateConfig({
1310
+ providers: patchProvider(entry.name, {
1311
+ [definition.key]: nextValue,
1312
+ }),
1313
+ }, `Updated ${definition.label.toLowerCase()} for "${entry.name}".`);
1314
+ setPrompt(null);
1315
+ },
1316
+ });
1317
+ },
1318
+ })),
886
1319
  ...(usageCount > 0
887
1320
  ? []
888
1321
  : [
@@ -904,6 +1337,75 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
904
1337
  ? `Used by ${usageCount} model(s). Rename updates references automatically; delete is disabled while in use.`
905
1338
  : "Edit shared provider settings or delete this provider.", items: items }));
906
1339
  };
1340
+ const renderAnthropicThinkingMenu = () => {
1341
+ if (screen.kind !== "config-provider-anthropic-thinking") {
1342
+ return null;
1343
+ }
1344
+ const entry = config.providers.find((provider) => provider.name === screen.name);
1345
+ if (!entry) {
1346
+ return null;
1347
+ }
1348
+ const current = "thinking" in entry.options ? entry.options.thinking : undefined;
1349
+ const items = [
1350
+ {
1351
+ label: current === undefined ? "Not set • current" : "Not set (clear value)",
1352
+ value: () => {
1353
+ updateConfig({ providers: patchProvider(entry.name, { thinking: undefined }) }, `Cleared thinking for "${entry.name}".`);
1354
+ setScreen({ kind: "config-provider-edit", name: entry.name });
1355
+ },
1356
+ },
1357
+ {
1358
+ label: current === "disabled" ? "disabled • current" : "disabled",
1359
+ value: () => {
1360
+ updateConfig({ providers: patchProvider(entry.name, { thinking: "disabled" }) }, `Updated thinking for "${entry.name}" to "disabled".`);
1361
+ setScreen({ kind: "config-provider-edit", name: entry.name });
1362
+ },
1363
+ },
1364
+ {
1365
+ label: current === "adaptive" ? "adaptive • current" : "adaptive",
1366
+ value: () => {
1367
+ updateConfig({ providers: patchProvider(entry.name, { thinking: "adaptive" }) }, `Updated thinking for "${entry.name}" to "adaptive".`);
1368
+ setScreen({ kind: "config-provider-edit", name: entry.name });
1369
+ },
1370
+ },
1371
+ {
1372
+ label: "Back",
1373
+ value: () => setScreen({ kind: "config-provider-edit", name: entry.name }),
1374
+ },
1375
+ ];
1376
+ return (_jsx(MenuScreen, { title: `Choose Thinking • ${entry.name}`, description: 'Pick one of the allowed values: "disabled", "adaptive", or clear it.', items: items }));
1377
+ };
1378
+ const renderOllamaThinkingMenu = () => {
1379
+ if (screen.kind !== "config-provider-ollama-thinking") {
1380
+ return null;
1381
+ }
1382
+ const entry = config.providers.find((provider) => provider.name === screen.name);
1383
+ if (!entry) {
1384
+ return null;
1385
+ }
1386
+ const current = "thinking" in entry.options ? entry.options.thinking : undefined;
1387
+ const items = [
1388
+ {
1389
+ label: current === undefined ? "Not set • current" : "Not set (clear value)",
1390
+ value: () => {
1391
+ updateConfig({ providers: patchProvider(entry.name, { thinking: undefined }) }, `Cleared thinking for "${entry.name}".`);
1392
+ setScreen({ kind: "config-provider-edit", name: entry.name });
1393
+ },
1394
+ },
1395
+ ...[false, true, "low", "medium", "high"].map((value) => ({
1396
+ label: current === value ? `${String(value)} • current` : String(value),
1397
+ value: () => {
1398
+ updateConfig({ providers: patchProvider(entry.name, { thinking: value }) }, `Updated thinking for "${entry.name}" to "${String(value)}".`);
1399
+ setScreen({ kind: "config-provider-edit", name: entry.name });
1400
+ },
1401
+ })),
1402
+ {
1403
+ label: "Back",
1404
+ value: () => setScreen({ kind: "config-provider-edit", name: entry.name }),
1405
+ },
1406
+ ];
1407
+ return (_jsx(MenuScreen, { title: `Choose Thinking • ${entry.name}`, description: 'Pick one of the allowed values: clear, false, true, "low", "medium", or "high".', items: items }));
1408
+ };
907
1409
  const renderProviderTypeMenu = () => {
908
1410
  if (screen.kind !== "config-provider-type") {
909
1411
  return null;
@@ -915,16 +1417,17 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
915
1417
  }
916
1418
  const items = [
917
1419
  ...SUPPORTED_PROVIDER_TYPES.map((provider) => ({
918
- label: provider === entry.options.provider
919
- ? `${provider} • current`
920
- : provider,
1420
+ label: provider === entry.provider ? `${provider} • current` : provider,
921
1421
  value: () => {
922
1422
  updateConfig({
923
- providers: patchProvider(entry.name, {
924
- provider,
925
- params: providerParamsTemplate(provider),
926
- }),
927
- }, `Updated provider type for "${entry.name}" to "${provider}" and scaffolded params.`);
1423
+ providers: config.providers.map((item) => item.name === entry.name
1424
+ ? {
1425
+ ...item,
1426
+ provider,
1427
+ options: providerOptionsTemplate(provider),
1428
+ }
1429
+ : item),
1430
+ }, `Updated provider type for "${entry.name}" to "${provider}" and scaffolded options.`);
928
1431
  setScreen({ kind: "config-provider-edit", name: entry.name });
929
1432
  },
930
1433
  })),
@@ -935,6 +1438,26 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
935
1438
  ];
936
1439
  return (_jsx(MenuScreen, { title: `Choose Provider Type • ${entry.name}`, description: "Pick which runtime provider this shared config targets.", items: items }));
937
1440
  };
1441
+ const renderProviderAddTypeMenu = () => {
1442
+ if (screen.kind !== "config-provider-add-type") {
1443
+ return null;
1444
+ }
1445
+ const { name } = screen;
1446
+ const items = [
1447
+ ...SUPPORTED_PROVIDER_TYPES.map((provider) => ({
1448
+ label: provider,
1449
+ value: () => {
1450
+ updateConfig({ providers: addProvider(name, provider) }, `Added provider "${name}" as "${provider}".`);
1451
+ setScreen({ kind: "config-provider-edit", name });
1452
+ },
1453
+ })),
1454
+ {
1455
+ label: "Back",
1456
+ value: () => setScreen({ kind: "config-providers" }),
1457
+ },
1458
+ ];
1459
+ return (_jsx(MenuScreen, { title: `Choose Provider Type • ${name}`, description: "Pick the runtime provider before creating this shared config.", items: items }));
1460
+ };
938
1461
  const renderProviderDeleteConfirm = () => {
939
1462
  if (screen.kind !== "config-provider-delete-confirm") {
940
1463
  return null;
@@ -1007,6 +1530,7 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
1007
1530
  }
1008
1531
  const isOnly = config.llms.length === 1;
1009
1532
  const isDefault = entry.default;
1533
+ const llmOptions = entry.options;
1010
1534
  const items = [
1011
1535
  {
1012
1536
  label: `Name • ${entry.name}`,
@@ -1033,7 +1557,7 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
1033
1557
  }),
1034
1558
  },
1035
1559
  {
1036
- label: `Provider • ${entry.options.provider}`,
1560
+ label: `Provider • ${entry.provider}`,
1037
1561
  value: () => setScreen({ kind: "config-llm-provider", name: entry.name }),
1038
1562
  },
1039
1563
  {
@@ -1042,6 +1566,12 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
1042
1566
  title: "Update model id",
1043
1567
  label: "Model",
1044
1568
  initialValue: entry.options.model,
1569
+ note: (() => {
1570
+ const providerType = config.providers.find((provider) => provider.name === entry.provider)?.provider;
1571
+ return providerType
1572
+ ? `Suggested default for ${providerType}: ${defaultModelForProviderType(providerType)}`
1573
+ : undefined;
1574
+ })(),
1045
1575
  onSubmit: async (value) => {
1046
1576
  const model = value.trim();
1047
1577
  if (!model) {
@@ -1052,20 +1582,28 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
1052
1582
  },
1053
1583
  }),
1054
1584
  },
1055
- {
1056
- label: `Params • ${paramsPreview(entry.options.params)}`,
1585
+ ...LLM_FIELD_DEFINITIONS.map((definition) => ({
1586
+ key: `llm-field:${entry.name}:${definition.key}`,
1587
+ label: `${definition.label} • ${formatTypedFieldValue(definition, llmOptions[definition.key])}`,
1057
1588
  value: () => promptValue({
1058
- title: "Update LLM params",
1059
- label: "Parameters",
1060
- initialValue: compactJson(entry.options.params),
1061
- placeholder: '{"temperature":0.7}',
1589
+ title: `Update ${definition.label}`,
1590
+ label: definition.label,
1591
+ initialValue: llmOptions[definition.key] !== undefined
1592
+ ? String(llmOptions[definition.key])
1593
+ : "",
1594
+ placeholder: definition.placeholder,
1595
+ note: definition.note,
1062
1596
  onSubmit: async (value) => {
1063
- const params = parseObjectRecord(value, "LLM params");
1064
- updateConfig({ llms: patchLlm(entry.name, { params }) }, "Updated LLM params.");
1597
+ const nextValue = parseTypedFieldValue(value, definition);
1598
+ updateConfig({
1599
+ llms: patchLlm(entry.name, {
1600
+ [definition.key]: nextValue,
1601
+ }),
1602
+ }, `Updated ${definition.label.toLowerCase()} for "${entry.name}".`);
1065
1603
  setPrompt(null);
1066
1604
  },
1067
1605
  }),
1068
- },
1606
+ })),
1069
1607
  {
1070
1608
  label: isDefault ? "Default • yes" : "Set as default",
1071
1609
  value: () => {
@@ -1109,11 +1647,22 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
1109
1647
  }
1110
1648
  const items = [
1111
1649
  ...config.providers.map((provider) => ({
1112
- label: provider.name === entry.options.provider
1650
+ label: provider.name === entry.provider
1113
1651
  ? `${provider.name} • current`
1114
- : `${provider.name} • ${provider.options.provider}`,
1652
+ : `${provider.name} • ${provider.provider}`,
1115
1653
  value: () => {
1116
- updateConfig({ llms: patchLlm(entry.name, { provider: provider.name }) }, `Updated provider for "${entry.name}" to "${provider.name}".`);
1654
+ updateConfig({
1655
+ llms: config.llms.map((llm) => llm.name === entry.name
1656
+ ? {
1657
+ ...llm,
1658
+ provider: provider.name,
1659
+ options: {
1660
+ ...llm.options,
1661
+ model: defaultModelForProviderType(provider.provider),
1662
+ },
1663
+ }
1664
+ : llm),
1665
+ }, `Updated provider for "${entry.name}" to "${provider.name}" and scaffolded its default model.`);
1117
1666
  setScreen({ kind: "config-llm-edit", name: entry.name });
1118
1667
  },
1119
1668
  })),
@@ -1167,7 +1716,7 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
1167
1716
  }),
1168
1717
  {
1169
1718
  label: "Back",
1170
- value: () => setScreen({ kind: "config-tools" }),
1719
+ value: () => setScreen({ kind: "config-general" }),
1171
1720
  },
1172
1721
  ];
1173
1722
  return (_jsx(MenuScreen, { title: "Prompts", description: "Choose which bundled harness prompt sections are included in future sessions.", items: items }));
@@ -1248,6 +1797,58 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
1248
1797
  ];
1249
1798
  return (_jsx(MenuScreen, { title: "Search", description: "Configure web search provider and credentials.", items: items }));
1250
1799
  };
1800
+ const renderMcpStdioEditMenu = () => {
1801
+ if (screen.kind !== "mcp-stdio-edit" || !mcpDraft) {
1802
+ return null;
1803
+ }
1804
+ const items = [
1805
+ ...MCP_STDIO_FIELDS.map((field) => ({
1806
+ key: `mcp-stdio:${field.key}`,
1807
+ label: `${field.label} • ${formatDraftFieldValue(field, mcpDraft[field.key])}`,
1808
+ value: () => editMcpDraftField(field, mcpDraft[field.key] ?? ""),
1809
+ })),
1810
+ {
1811
+ label: "Save",
1812
+ value: () => saveMcpStdioDraft(screen.originalName),
1813
+ },
1814
+ {
1815
+ label: "Back",
1816
+ value: () => {
1817
+ setMcpDraft(null);
1818
+ setScreen({ kind: "mcp" });
1819
+ },
1820
+ },
1821
+ ];
1822
+ return (_jsx(MenuScreen, { title: `${screen.originalName ? "Edit" : "Add"} stdio server`, description: "Select a field to edit, then save when you're done.", items: items }));
1823
+ };
1824
+ const renderMcpRemoteEditMenu = () => {
1825
+ if (screen.kind !== "mcp-remote-edit" || !mcpDraft) {
1826
+ return null;
1827
+ }
1828
+ const fields = [
1829
+ ...MCP_REMOTE_BASE_FIELDS,
1830
+ ...(isTruthyToggle(mcpDraft.oauthEnabled) ? MCP_REMOTE_OAUTH_FIELDS : []),
1831
+ ];
1832
+ const items = [
1833
+ ...fields.map((field) => ({
1834
+ key: `mcp-remote:${field.key}`,
1835
+ label: `${field.label} • ${formatDraftFieldValue(field, mcpDraft[field.key])}`,
1836
+ value: () => editMcpDraftField(field, mcpDraft[field.key] ?? ""),
1837
+ })),
1838
+ {
1839
+ label: "Save",
1840
+ value: () => saveMcpRemoteDraft(screen.transportType, screen.originalName),
1841
+ },
1842
+ {
1843
+ label: "Back",
1844
+ value: () => {
1845
+ setMcpDraft(null);
1846
+ setScreen({ kind: "mcp" });
1847
+ },
1848
+ },
1849
+ ];
1850
+ 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 }));
1851
+ };
1251
1852
  const renderMcpMenu = () => {
1252
1853
  const serverItems = mcpServers.map((server) => {
1253
1854
  const oauthStatus = mcpAuthStatuses[server.name];
@@ -1262,10 +1863,10 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
1262
1863
  : undefined,
1263
1864
  value: () => {
1264
1865
  if (server.transport.type === "stdio") {
1265
- promptForStdio(server.name, server.transport);
1866
+ openMcpStdioEditor(server.name, server.transport);
1266
1867
  }
1267
1868
  else {
1268
- promptForRemote(server.name, server.transport.type, server.transport);
1869
+ openMcpRemoteEditor(server.transport.type, server.name, server.transport);
1269
1870
  }
1270
1871
  },
1271
1872
  };
@@ -1273,15 +1874,15 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
1273
1874
  const items = [
1274
1875
  {
1275
1876
  label: "Add stdio server",
1276
- value: () => promptForServerName("stdio"),
1877
+ value: () => openMcpStdioEditor(),
1277
1878
  },
1278
1879
  {
1279
1880
  label: "Add streamable HTTP server",
1280
- value: () => promptForServerName("streamable-http"),
1881
+ value: () => openMcpRemoteEditor("streamable-http"),
1281
1882
  },
1282
1883
  {
1283
1884
  label: "Add SSE server",
1284
- value: () => promptForServerName("sse"),
1885
+ value: () => openMcpRemoteEditor("sse"),
1285
1886
  },
1286
1887
  ...serverItems,
1287
1888
  {
@@ -1483,14 +2084,22 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
1483
2084
  switch (screen.kind) {
1484
2085
  case "home":
1485
2086
  return renderHome();
2087
+ case "config-general":
2088
+ return renderGeneralMenu();
1486
2089
  case "config":
1487
2090
  return renderConfigMenu();
1488
2091
  case "config-providers":
1489
2092
  return renderProvidersMenu();
2093
+ case "config-provider-add-type":
2094
+ return renderProviderAddTypeMenu();
1490
2095
  case "config-provider-edit":
1491
2096
  return renderProviderEditMenu();
1492
2097
  case "config-provider-type":
1493
2098
  return renderProviderTypeMenu();
2099
+ case "config-provider-anthropic-thinking":
2100
+ return renderAnthropicThinkingMenu();
2101
+ case "config-provider-ollama-thinking":
2102
+ return renderOllamaThinkingMenu();
1494
2103
  case "config-provider-delete-confirm":
1495
2104
  return renderProviderDeleteConfirm();
1496
2105
  case "config-llms":
@@ -1511,6 +2120,10 @@ export function ConfigureApp({ config, mcpConfig, mcpManager, skills, onExit, })
1511
2120
  return renderSearchProviderMenu();
1512
2121
  case "mcp":
1513
2122
  return renderMcpMenu();
2123
+ case "mcp-stdio-edit":
2124
+ return renderMcpStdioEditMenu();
2125
+ case "mcp-remote-edit":
2126
+ return renderMcpRemoteEditMenu();
1514
2127
  case "mcp-delete-confirm":
1515
2128
  return renderMcpDeleteConfirm();
1516
2129
  case "skills":