@townco/cli 0.1.3 → 0.1.5

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.
@@ -0,0 +1 @@
1
+ export declare function configureCommand(): Promise<void>;
@@ -0,0 +1,146 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import inquirer from "inquirer";
6
+ const ENV_KEYS = [
7
+ {
8
+ key: "ANTHROPIC_API_KEY",
9
+ description: "Anthropic API key for Claude models",
10
+ required: true,
11
+ },
12
+ {
13
+ key: "EXA_API_KEY",
14
+ description: "Exa API key for web search tool",
15
+ required: false,
16
+ },
17
+ {
18
+ key: "OPENAI_API_KEY",
19
+ description: "OpenAI API key (optional, for OpenAI models)",
20
+ required: false,
21
+ },
22
+ ];
23
+ function getConfigDir() {
24
+ return join(homedir(), ".config", "town");
25
+ }
26
+ function getEnvFilePath() {
27
+ return join(getConfigDir(), ".env");
28
+ }
29
+ async function loadExistingEnv() {
30
+ const envPath = getEnvFilePath();
31
+ if (!existsSync(envPath)) {
32
+ return {};
33
+ }
34
+ const content = await readFile(envPath, "utf-8");
35
+ const config = {};
36
+ for (const line of content.split("\n")) {
37
+ const trimmed = line.trim();
38
+ if (!trimmed || trimmed.startsWith("#"))
39
+ continue;
40
+ const [key, ...valueParts] = trimmed.split("=");
41
+ const value = valueParts.join("=").trim();
42
+ if (key && value) {
43
+ config[key.trim()] = value;
44
+ }
45
+ }
46
+ return config;
47
+ }
48
+ async function saveEnv(config) {
49
+ const configDir = getConfigDir();
50
+ await mkdir(configDir, { recursive: true });
51
+ const lines = [
52
+ "# Town CLI Configuration",
53
+ "# Environment variables for Town agents",
54
+ "",
55
+ ];
56
+ for (const { key, description } of ENV_KEYS) {
57
+ const value = config[key];
58
+ if (value) {
59
+ lines.push(`# ${description}`);
60
+ lines.push(`${key}=${value}`);
61
+ lines.push("");
62
+ }
63
+ }
64
+ await writeFile(getEnvFilePath(), lines.join("\n"), "utf-8");
65
+ }
66
+ export async function configureCommand() {
67
+ console.log("🔧 Town Configuration\n");
68
+ const existingConfig = await loadExistingEnv();
69
+ const hasExisting = Object.keys(existingConfig).length > 0;
70
+ if (hasExisting) {
71
+ console.log("Found existing configuration:\n");
72
+ for (const { key } of ENV_KEYS) {
73
+ const value = existingConfig[key];
74
+ if (value) {
75
+ const masked = `${value.slice(0, 4)}...${value.slice(-4)}`;
76
+ console.log(` ${key}: ${masked}`);
77
+ }
78
+ }
79
+ console.log("");
80
+ const { action } = await inquirer.prompt([
81
+ {
82
+ type: "list",
83
+ name: "action",
84
+ message: "What would you like to do?",
85
+ choices: [
86
+ { name: "Update existing keys", value: "update" },
87
+ { name: "Add new keys", value: "add" },
88
+ { name: "Reconfigure all keys", value: "reconfigure" },
89
+ { name: "Cancel", value: "cancel" },
90
+ ],
91
+ },
92
+ ]);
93
+ if (action === "cancel") {
94
+ console.log("Configuration cancelled.");
95
+ return;
96
+ }
97
+ if (action === "reconfigure") {
98
+ // Clear existing config for fresh start
99
+ Object.keys(existingConfig).forEach((key) => {
100
+ delete existingConfig[key];
101
+ });
102
+ }
103
+ }
104
+ const newConfig = { ...existingConfig };
105
+ // Prompt for each key
106
+ for (const { key, description, required } of ENV_KEYS) {
107
+ const hasValue = !!existingConfig[key];
108
+ if (hasValue && !hasExisting) {
109
+ // Skip if we're only adding new keys and this one exists
110
+ continue;
111
+ }
112
+ const { shouldSet } = await inquirer.prompt([
113
+ {
114
+ type: "confirm",
115
+ name: "shouldSet",
116
+ message: hasValue
117
+ ? `Update ${key}? (${description})`
118
+ : `Set ${key}? (${description})`,
119
+ default: required || !hasValue,
120
+ },
121
+ ]);
122
+ if (shouldSet) {
123
+ const { value } = await inquirer.prompt([
124
+ {
125
+ type: "password",
126
+ name: "value",
127
+ message: `Enter ${key}:`,
128
+ mask: "*",
129
+ validate: (input) => {
130
+ if (required && !input.trim()) {
131
+ return "This key is required";
132
+ }
133
+ return true;
134
+ },
135
+ },
136
+ ]);
137
+ if (value.trim()) {
138
+ newConfig[key] = value.trim();
139
+ }
140
+ }
141
+ }
142
+ // Save configuration
143
+ await saveEnv(newConfig);
144
+ console.log(`\n✅ Configuration saved to ${getEnvFilePath()}`);
145
+ console.log("\nThese environment variables will be automatically loaded when running agents.");
146
+ }
@@ -1,9 +1,8 @@
1
1
  interface CreateCommandProps {
2
- name?: string;
3
- model?: string;
4
- tools?: readonly string[];
5
- systemPrompt?: string;
6
- overwrite?: boolean;
2
+ name?: string;
3
+ model?: string;
4
+ tools?: readonly string[];
5
+ systemPrompt?: string;
6
+ overwrite?: boolean;
7
7
  }
8
8
  export declare function createCommand(props: CreateCommandProps): Promise<void>;
9
- export {};
@@ -3,8 +3,8 @@ import { spawn } from "node:child_process";
3
3
  import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
- import { scaffoldAgent } from "@townco/agent/scaffold";
7
- import { InputBox, MultiSelect, SingleSelect } from "@townco/ui/tui";
6
+ import { scaffoldAgent } from "@town/agent/scaffold";
7
+ import { InputBox, MultiSelect, SingleSelect } from "@town/ui/tui";
8
8
  import { Box, render, Text, useInput } from "ink";
9
9
  import TextInput from "ink-text-input";
10
10
  import { useEffect, useState } from "react";
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { agentExists, deleteAgent } from "@townco/agent/storage";
3
- import { SingleSelect } from "@townco/ui/tui";
2
+ import { agentExists, deleteAgent } from "@town/agent/storage";
3
+ import { SingleSelect } from "@town/ui/tui";
4
4
  import { Box, render, Text } from "ink";
5
5
  import { useState } from "react";
6
6
  function DeleteApp({ name }) {
@@ -1,6 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { agentExists, getAgentPath } from "@townco/agent/storage";
3
+ import { agentExists, getAgentPath } from "@town/agent/storage";
4
4
  import { createCommand } from "./create.js";
5
5
  async function loadAgentConfig(name) {
6
6
  try {
@@ -1,6 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { getAgentPath, listAgents } from "@townco/agent/storage";
3
+ import { getAgentPath, listAgents } from "@town/agent/storage";
4
4
  async function getAgentInfo(name) {
5
5
  try {
6
6
  const agentPath = getAgentPath(name);
@@ -0,0 +1,14 @@
1
+ interface MCPAddProps {
2
+ name?: string;
3
+ url?: string;
4
+ command?: string;
5
+ args?: readonly string[];
6
+ }
7
+ declare function MCPAddApp({
8
+ name: initialName,
9
+ url: initialUrl,
10
+ command: initialCommand,
11
+ args: initialArgs,
12
+ }: MCPAddProps): import("react/jsx-runtime").JSX.Element | null;
13
+ export default MCPAddApp;
14
+ export declare function runMCPAdd(props?: MCPAddProps): Promise<void>;
@@ -0,0 +1,445 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { readFileSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { getAgentPath, listAgents } from "@town/agent/storage";
5
+ import { MultiSelect, SingleSelect } from "@town/ui/tui";
6
+ import { Box, render, Text, useApp, useInput } from "ink";
7
+ import TextInput from "ink-text-input";
8
+ import { useEffect, useState } from "react";
9
+ import { mcpConfigExists, saveMCPConfig } from "../lib/mcp-storage";
10
+ function TextInputStage({ title, value, onChange, onSubmit, onCancel, placeholder, }) {
11
+ useInput((_input, key) => {
12
+ if (key.escape) {
13
+ onCancel();
14
+ }
15
+ });
16
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: title }) }), _jsxs(Box, { children: [_jsxs(Text, { children: [">", " "] }), _jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit, ...(placeholder && { placeholder }) })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: Continue \u2022 Esc: Back" }) })] }));
17
+ }
18
+ function TransportSelectStage({ selected, onChange, onNext, onCancel, }) {
19
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Select transport type:" }) }), _jsx(SingleSelect, { options: [
20
+ {
21
+ label: "stdio",
22
+ value: "stdio",
23
+ description: "Run MCP server as a local process",
24
+ },
25
+ {
26
+ label: "HTTP",
27
+ value: "http",
28
+ description: "Connect to MCP server over HTTP",
29
+ },
30
+ ], selected: selected, onChange: (transport) => onChange(transport), onSubmit: (transport) => onNext(transport), onCancel: onCancel })] }));
31
+ }
32
+ function NameInputStage({ value, onChange, onNext, onBack, onError, }) {
33
+ const handleSubmit = () => {
34
+ const trimmed = value.trim();
35
+ if (!trimmed) {
36
+ return;
37
+ }
38
+ // Check for duplicate
39
+ if (mcpConfigExists(trimmed)) {
40
+ onError(`MCP server "${trimmed}" already exists`);
41
+ return;
42
+ }
43
+ onNext(trimmed);
44
+ };
45
+ return (_jsx(TextInputStage, { title: "Enter MCP server name:", value: value, onChange: onChange, onSubmit: handleSubmit, onCancel: onBack, placeholder: "my-mcp-server" }));
46
+ }
47
+ function StdioCommandStage({ serverName, value, onChange, onNext, onBack, }) {
48
+ const handleSubmit = () => {
49
+ const trimmed = value.trim();
50
+ if (!trimmed) {
51
+ return;
52
+ }
53
+ onNext(trimmed);
54
+ };
55
+ return (_jsx(TextInputStage, { title: `Enter command to run MCP server: ${serverName}`, value: value, onChange: onChange, onSubmit: handleSubmit, onCancel: onBack, placeholder: "npx @modelcontextprotocol/server-filesystem" }));
56
+ }
57
+ function StdioArgsStage({ serverName, args, onAddArg, onRemoveLastArg, onNext, onBack, }) {
58
+ const [argInput, setArgInput] = useState("");
59
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, children: ["Add arguments for: ", serverName, " (", args.length, " added)"] }) }), args.length > 0 && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Current arguments:" }), args.map((arg, index) => (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { children: [index + 1, ". ", arg] }) }, arg)))] })), _jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { children: [">", " "] }), _jsx(TextInput, { value: argInput, onChange: setArgInput, onSubmit: () => {
60
+ const trimmed = argInput.trim();
61
+ if (trimmed) {
62
+ onAddArg(trimmed);
63
+ setArgInput("");
64
+ }
65
+ }, placeholder: "Enter argument (or leave empty and press Enter to continue)" })] }), _jsx(SingleSelect, { options: [
66
+ {
67
+ label: "Done adding arguments",
68
+ value: "done",
69
+ description: "Continue to review",
70
+ },
71
+ {
72
+ label: "Remove last argument",
73
+ value: "remove",
74
+ description: args.length > 0
75
+ ? `Remove: ${args[args.length - 1]}`
76
+ : "No arguments to remove",
77
+ },
78
+ ], selected: null, onChange: () => { }, onSubmit: (value) => {
79
+ if (value === "done") {
80
+ onNext();
81
+ }
82
+ else if (value === "remove" && args.length > 0) {
83
+ onRemoveLastArg();
84
+ }
85
+ }, onCancel: onBack })] }));
86
+ }
87
+ function HttpUrlStage({ serverName, value, onChange, onNext, onBack, onError, }) {
88
+ const handleSubmit = () => {
89
+ const trimmed = value.trim();
90
+ if (!trimmed) {
91
+ return;
92
+ }
93
+ // Basic URL validation
94
+ try {
95
+ new URL(trimmed);
96
+ }
97
+ catch {
98
+ onError("Invalid URL format");
99
+ return;
100
+ }
101
+ onNext(trimmed);
102
+ };
103
+ return (_jsx(TextInputStage, { title: `Enter HTTP URL for MCP server: ${serverName}`, value: value, onChange: onChange, onSubmit: handleSubmit, onCancel: onBack, placeholder: "http://localhost:3000/mcp" }));
104
+ }
105
+ function ReviewStage({ config, onEdit, onSave, onBack }) {
106
+ const [reviewSelection, setReviewSelection] = useState(null);
107
+ const reviewOptions = [
108
+ {
109
+ label: "Save configuration",
110
+ value: "continue",
111
+ description: "Save and finish",
112
+ },
113
+ {
114
+ label: "Edit name",
115
+ value: "name",
116
+ description: "Change the MCP server name",
117
+ },
118
+ {
119
+ label: "Edit transport",
120
+ value: "transport",
121
+ description: "Change the transport type",
122
+ },
123
+ ];
124
+ if (config.transport === "stdio") {
125
+ reviewOptions.push({
126
+ label: "Edit command",
127
+ value: "stdioCommand",
128
+ description: "Change the command",
129
+ }, {
130
+ label: "Edit arguments",
131
+ value: "stdioArgs",
132
+ description: "Change the arguments",
133
+ });
134
+ }
135
+ else {
136
+ reviewOptions.push({
137
+ label: "Edit URL",
138
+ value: "httpUrl",
139
+ description: "Change the HTTP URL",
140
+ });
141
+ }
142
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Review MCP Server Configuration:" }) }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Name: " }), config.name] }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Transport: " }), config.transport] }), config.transport === "stdio" && (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Command: " }), config.command] }), config.args && config.args.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Arguments:" }), config.args.map((arg, index) => (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { dimColor: true, children: [index + 1, ". ", arg] }) }, arg)))] }))] })), config.transport === "http" && (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "URL: " }), config.url] }))] }), _jsx(SingleSelect, { options: reviewOptions, selected: reviewSelection, onChange: setReviewSelection, onSubmit: (value) => {
143
+ setReviewSelection(value);
144
+ if (value === "continue") {
145
+ onSave();
146
+ }
147
+ else if (value === "name" ||
148
+ value === "transport" ||
149
+ value === "stdioCommand" ||
150
+ value === "stdioArgs" ||
151
+ value === "httpUrl") {
152
+ onEdit(value);
153
+ }
154
+ }, onCancel: onBack })] }));
155
+ }
156
+ function NoAgentsMessage({ onNext }) {
157
+ useEffect(() => {
158
+ const timer = setTimeout(() => {
159
+ onNext();
160
+ }, 1000);
161
+ return () => clearTimeout(timer);
162
+ }, [onNext]);
163
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "No agents found. MCP server will be saved globally." }), _jsx(Text, { dimColor: true, children: "You can attach it to agents later." })] }));
164
+ }
165
+ function AgentSelectionStage({ selectedAgents, onSelectedAgentsChange, onNext, onBack, }) {
166
+ const [availableAgents, setAvailableAgents] = useState([]);
167
+ const [isLoading, setIsLoading] = useState(true);
168
+ useEffect(() => {
169
+ // Fetch available agents
170
+ const fetchAgents = async () => {
171
+ try {
172
+ const agents = await listAgents();
173
+ setAvailableAgents(agents);
174
+ }
175
+ catch (error) {
176
+ console.error("Error fetching agents:", error);
177
+ setAvailableAgents([]);
178
+ }
179
+ finally {
180
+ setIsLoading(false);
181
+ }
182
+ };
183
+ fetchAgents();
184
+ }, []);
185
+ // If still loading, show loading message
186
+ if (isLoading) {
187
+ return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { children: "Loading agents..." }) }));
188
+ }
189
+ // If no agents available, show info message and auto-proceed
190
+ if (availableAgents.length === 0) {
191
+ return _jsx(NoAgentsMessage, { onNext: onNext });
192
+ }
193
+ // Show agent selection UI
194
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Attach MCP server to agents (optional):" }) }), _jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Select which agents should have access to this MCP server." }), _jsx(Text, { dimColor: true, children: "You can skip this step and attach agents later." })] }), _jsx(MultiSelect, { options: availableAgents.map((agent) => ({
195
+ label: agent,
196
+ value: agent,
197
+ })), selected: selectedAgents, onChange: onSelectedAgentsChange, onSubmit: onNext, onCancel: onBack })] }));
198
+ }
199
+ function DoneStage({ config, status, error, attachedAgents }) {
200
+ const { exit } = useApp();
201
+ useEffect(() => {
202
+ if (status === "done") {
203
+ setImmediate(() => {
204
+ exit();
205
+ });
206
+ }
207
+ }, [status, exit]);
208
+ if (status === "saving") {
209
+ return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { children: "\u23F3 Saving MCP server configuration..." }) }));
210
+ }
211
+ if (status === "error") {
212
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", children: "\u274C Error saving MCP server" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: error }) })] }));
213
+ }
214
+ if (status === "done") {
215
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", children: "\u2705 MCP server saved successfully!" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Name: ", config.name] }) }), attachedAgents.length > 0 && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsxs(Text, { dimColor: true, children: ["Attached to agents: ", attachedAgents.join(", ")] }) }))] }));
216
+ }
217
+ return null;
218
+ }
219
+ // ============================================================================
220
+ // Main Component
221
+ // ============================================================================
222
+ function MCPAddApp({ name: initialName, url: initialUrl, command: initialCommand, args: initialArgs, }) {
223
+ // Determine the starting stage based on what's provided
224
+ const determineInitialStage = () => {
225
+ // If command provided, transport is stdio
226
+ if (initialCommand) {
227
+ return initialName ? "stdioArgs" : "name";
228
+ }
229
+ // If URL provided, transport is http
230
+ if (initialUrl) {
231
+ return initialName ? "review" : "name";
232
+ }
233
+ return "transport";
234
+ };
235
+ const { exit } = useApp();
236
+ const [stage, setStage] = useState(determineInitialStage());
237
+ const [config, setConfig] = useState({
238
+ ...(initialName && { name: initialName }),
239
+ ...(initialCommand && { transport: "stdio" }),
240
+ ...(initialUrl && !initialCommand && { transport: "http" }),
241
+ ...(initialCommand && { command: initialCommand }),
242
+ ...(initialArgs && initialArgs.length > 0 && { args: [...initialArgs] }),
243
+ ...(initialUrl && { url: initialUrl }),
244
+ });
245
+ const [nameInput, setNameInput] = useState(initialName || "");
246
+ const [commandInput, setCommandInput] = useState(initialCommand || "");
247
+ const [urlInput, setUrlInput] = useState(initialUrl || "");
248
+ const [isEditingFromReview, setIsEditingFromReview] = useState(false);
249
+ const [saveStatus, setSaveStatus] = useState("pending");
250
+ const [saveError, setSaveError] = useState(null);
251
+ const [selectedAgents, setSelectedAgents] = useState([]);
252
+ const handleSave = (agentsToAttach) => {
253
+ setSaveStatus("saving");
254
+ // Save the config
255
+ try {
256
+ if (!config.name || !config.transport) {
257
+ setSaveStatus("error");
258
+ setSaveError("Missing required fields");
259
+ return;
260
+ }
261
+ if (config.transport === "stdio" &&
262
+ (!config.command || !config.command.trim())) {
263
+ setSaveStatus("error");
264
+ setSaveError("Command is required for stdio transport");
265
+ return;
266
+ }
267
+ if (config.transport === "http" && (!config.url || !config.url.trim())) {
268
+ setSaveStatus("error");
269
+ setSaveError("URL is required for http transport");
270
+ return;
271
+ }
272
+ const mcpConfig = {
273
+ name: config.name,
274
+ transport: config.transport,
275
+ ...(config.transport === "stdio" && {
276
+ command: config.command,
277
+ args: config.args,
278
+ }),
279
+ ...(config.transport === "http" && {
280
+ url: config.url,
281
+ }),
282
+ };
283
+ // Save to global MCP storage
284
+ saveMCPConfig(mcpConfig);
285
+ // Update selected agents
286
+ for (const agentName of agentsToAttach) {
287
+ try {
288
+ const agentPath = getAgentPath(agentName);
289
+ const agentJsonPath = join(agentPath, "agent.json");
290
+ // Read existing agent.json
291
+ const agentJsonContent = readFileSync(agentJsonPath, "utf-8");
292
+ const agentDef = JSON.parse(agentJsonContent);
293
+ // Add MCP config to mcps array (create array if doesn't exist)
294
+ if (!agentDef.mcps) {
295
+ agentDef.mcps = [];
296
+ }
297
+ // Check if this MCP server is already in the agent's config
298
+ const existingIndex = agentDef.mcps.findIndex((mcp) => mcp.name === mcpConfig.name);
299
+ if (existingIndex >= 0) {
300
+ // Update existing config
301
+ agentDef.mcps[existingIndex] = mcpConfig;
302
+ }
303
+ else {
304
+ // Add new config
305
+ agentDef.mcps.push(mcpConfig);
306
+ }
307
+ // Write back to agent.json
308
+ writeFileSync(agentJsonPath, JSON.stringify(agentDef, null, 2));
309
+ }
310
+ catch (error) {
311
+ console.error(`Error updating agent ${agentName}:`, error);
312
+ // Continue with other agents even if one fails
313
+ }
314
+ }
315
+ setSaveStatus("done");
316
+ }
317
+ catch (error) {
318
+ setSaveStatus("error");
319
+ setSaveError(error instanceof Error ? error.message : "Unknown error occurred");
320
+ }
321
+ };
322
+ // Transport selection stage
323
+ if (stage === "transport") {
324
+ return (_jsx(TransportSelectStage, { selected: config.transport || null, onChange: (transport) => setConfig({ ...config, transport }), onNext: (transport) => {
325
+ setConfig({ ...config, transport });
326
+ if (isEditingFromReview) {
327
+ setIsEditingFromReview(false);
328
+ setStage("review");
329
+ }
330
+ else {
331
+ setStage("name");
332
+ }
333
+ }, onCancel: () => exit() }));
334
+ }
335
+ // Name input stage
336
+ if (stage === "name") {
337
+ return (_jsx(NameInputStage, { value: nameInput, onChange: setNameInput, onNext: (name) => {
338
+ setConfig({ ...config, name });
339
+ if (isEditingFromReview) {
340
+ setIsEditingFromReview(false);
341
+ setStage("review");
342
+ }
343
+ else if (config.transport === "stdio") {
344
+ setStage("stdioCommand");
345
+ }
346
+ else {
347
+ setStage("httpUrl");
348
+ }
349
+ }, onBack: () => {
350
+ if (initialCommand || initialUrl) {
351
+ exit();
352
+ }
353
+ else {
354
+ setStage("transport");
355
+ }
356
+ }, onError: setSaveError }));
357
+ }
358
+ // Stdio command input stage
359
+ if (stage === "stdioCommand") {
360
+ return (_jsx(StdioCommandStage, { serverName: config.name || "", value: commandInput, onChange: setCommandInput, onNext: (command) => {
361
+ setConfig({ ...config, command });
362
+ if (isEditingFromReview) {
363
+ setIsEditingFromReview(false);
364
+ setStage("review");
365
+ }
366
+ else {
367
+ setStage("stdioArgs");
368
+ }
369
+ }, onBack: () => {
370
+ if (initialCommand) {
371
+ exit();
372
+ }
373
+ else {
374
+ setStage("name");
375
+ }
376
+ } }));
377
+ }
378
+ // Stdio args input stage
379
+ if (stage === "stdioArgs") {
380
+ return (_jsx(StdioArgsStage, { serverName: config.name || "", args: config.args || [], onAddArg: (arg) => setConfig({ ...config, args: [...(config.args || []), arg] }), onRemoveLastArg: () => setConfig({ ...config, args: (config.args || []).slice(0, -1) }), onNext: () => {
381
+ if (isEditingFromReview) {
382
+ setIsEditingFromReview(false);
383
+ }
384
+ setStage("review");
385
+ }, onBack: () => setStage("stdioCommand") }));
386
+ }
387
+ // HTTP URL input stage
388
+ if (stage === "httpUrl") {
389
+ return (_jsx(HttpUrlStage, { serverName: config.name || "", value: urlInput, onChange: setUrlInput, onNext: (url) => {
390
+ setConfig({ ...config, url });
391
+ if (isEditingFromReview) {
392
+ setIsEditingFromReview(false);
393
+ }
394
+ setStage("review");
395
+ }, onBack: () => {
396
+ if (initialUrl) {
397
+ exit();
398
+ }
399
+ else {
400
+ setStage("name");
401
+ }
402
+ }, onError: setSaveError }));
403
+ }
404
+ // Review stage
405
+ if (stage === "review") {
406
+ return (_jsx(ReviewStage, { config: config, onEdit: (editStage) => {
407
+ setIsEditingFromReview(true);
408
+ setStage(editStage);
409
+ }, onSave: () => {
410
+ setStage("agentSelection");
411
+ }, onBack: () => {
412
+ if (config.transport === "stdio") {
413
+ setStage("stdioArgs");
414
+ }
415
+ else {
416
+ setStage("httpUrl");
417
+ }
418
+ } }));
419
+ }
420
+ // Agent selection stage
421
+ if (stage === "agentSelection") {
422
+ return (_jsx(AgentSelectionStage, { selectedAgents: selectedAgents, onSelectedAgentsChange: setSelectedAgents, onNext: () => {
423
+ handleSave(selectedAgents);
424
+ setStage("done");
425
+ }, onBack: () => setStage("review") }));
426
+ }
427
+ // Done stage
428
+ if (stage === "done") {
429
+ return (_jsx(DoneStage, { config: config, status: saveStatus, error: saveError, attachedAgents: selectedAgents }));
430
+ }
431
+ return null;
432
+ }
433
+ // ============================================================================
434
+ // Export and Runner
435
+ // ============================================================================
436
+ export default MCPAddApp;
437
+ export async function runMCPAdd(props = {}) {
438
+ // Set stdin to raw mode to capture input
439
+ if (process.stdin.isTTY) {
440
+ process.stdin.setRawMode(true);
441
+ }
442
+ const { waitUntilExit } = render(_jsx(MCPAddApp, { ...props }));
443
+ // Wait for the app to exit before returning
444
+ await waitUntilExit();
445
+ }
@@ -0,0 +1,3 @@
1
+ declare function MCPListApp(): import("react/jsx-runtime").JSX.Element;
2
+ export default MCPListApp;
3
+ export declare function runMCPList(): Promise<void>;