@townco/cli 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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 {};
@@ -1,13 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { spawn } from "node:child_process";
3
- import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
4
- import { tmpdir } from "node:os";
5
- import { join } from "node:path";
6
2
  import { scaffoldAgent } from "@townco/agent/scaffold";
7
3
  import { InputBox, MultiSelect, SingleSelect } from "@townco/ui/tui";
8
4
  import { Box, render, Text, useInput } from "ink";
9
5
  import TextInput from "ink-text-input";
10
6
  import { useEffect, useState } from "react";
7
+ import { openInEditor } from "../lib/editor-utils";
11
8
  const AVAILABLE_MODELS = [
12
9
  {
13
10
  label: "Claude Sonnet 4.5",
@@ -55,70 +52,6 @@ function NameInput({ nameInput, setNameInput, onSubmit }) {
55
52
  });
56
53
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Enter agent name:" }) }), _jsxs(Box, { children: [_jsxs(Text, { children: [">", " "] }), _jsx(TextInput, { value: nameInput, onChange: setNameInput, onSubmit: onSubmit })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: Continue \u2022 Esc: Cancel" }) })] }));
57
54
  }
58
- async function openEditor(initialContent) {
59
- const tempFile = join(tmpdir(), `agent-prompt-${Date.now()}.txt`);
60
- try {
61
- // Write initial content
62
- writeFileSync(tempFile, initialContent, "utf-8");
63
- // Try $EDITOR first
64
- const editor = process.env.EDITOR;
65
- if (editor) {
66
- try {
67
- await new Promise((resolve, reject) => {
68
- const child = spawn(editor, [tempFile], {
69
- stdio: "inherit",
70
- });
71
- child.on("close", (code) => {
72
- if (code === 0)
73
- resolve();
74
- else
75
- reject(new Error(`Editor exited with code ${code}`));
76
- });
77
- child.on("error", reject);
78
- });
79
- const content = readFileSync(tempFile, "utf-8");
80
- unlinkSync(tempFile);
81
- return content;
82
- }
83
- catch (_error) {
84
- // Fall through to try 'code'
85
- }
86
- }
87
- // Try 'code' (VS Code) as fallback
88
- try {
89
- await new Promise((resolve, reject) => {
90
- const child = spawn("code", ["--wait", tempFile], {
91
- stdio: "inherit",
92
- });
93
- child.on("close", (code) => {
94
- if (code === 0)
95
- resolve();
96
- else
97
- reject(new Error(`Code exited with code ${code}`));
98
- });
99
- child.on("error", reject);
100
- });
101
- const content = readFileSync(tempFile, "utf-8");
102
- unlinkSync(tempFile);
103
- return content;
104
- }
105
- catch (_error) {
106
- // Clean up and return null to signal fallback to inline
107
- unlinkSync(tempFile);
108
- return null;
109
- }
110
- }
111
- catch (_error) {
112
- // Clean up temp file if it exists
113
- try {
114
- unlinkSync(tempFile);
115
- }
116
- catch {
117
- // Ignore cleanup errors
118
- }
119
- return null;
120
- }
121
- }
122
55
  function CreateApp({ name: initialName, model: initialModel, tools: initialTools, systemPrompt: initialSystemPrompt, overwrite = false, }) {
123
56
  // Determine the starting stage based on what's provided
124
57
  const determineInitialStage = () => {
@@ -150,13 +83,14 @@ function CreateApp({ name: initialName, model: initialModel, tools: initialTools
150
83
  const [agentPath, setAgentPath] = useState(null);
151
84
  // Handle opening editor when systemPrompt stage is entered from review
152
85
  useEffect(() => {
153
- if (stage === "systemPrompt" &&
154
- isEditingFromReview &&
155
- !isEditingPrompt &&
156
- promptEditMode === null) {
157
- // Trigger editor opening
158
- setIsEditingPrompt(true);
159
- openEditor(agentDef.systemPrompt || "You are a helpful assistant.").then((editorContent) => {
86
+ (async () => {
87
+ if (stage === "systemPrompt" &&
88
+ isEditingFromReview &&
89
+ !isEditingPrompt &&
90
+ promptEditMode === null) {
91
+ // Trigger editor opening
92
+ setIsEditingPrompt(true);
93
+ const editorContent = await openInEditor(agentDef.systemPrompt || "You are a helpful assistant.");
160
94
  if (editorContent !== null) {
161
95
  // Editor worked
162
96
  setAgentDef({ ...agentDef, systemPrompt: editorContent });
@@ -170,8 +104,8 @@ function CreateApp({ name: initialName, model: initialModel, tools: initialTools
170
104
  setIsEditingPrompt(false);
171
105
  setSystemPromptInput(agentDef.systemPrompt || "You are a helpful assistant.");
172
106
  }
173
- });
174
- }
107
+ }
108
+ })();
175
109
  }, [stage, isEditingFromReview, isEditingPrompt, promptEditMode, agentDef]);
176
110
  // Handle scaffolding when entering "done" stage
177
111
  useEffect(() => {
@@ -197,20 +131,10 @@ function CreateApp({ name: initialName, model: initialModel, tools: initialTools
197
131
  if (result.success) {
198
132
  setScaffoldStatus("done");
199
133
  setAgentPath(result.path);
200
- // Output in the same format as 'town list'
201
- const modelLabel = model.replace("claude-", "");
202
- console.log(`\n\x1b[1mAgent created successfully!\x1b[0m\n`);
203
- console.log(` \x1b[32m●\x1b[0m \x1b[1m${name}\x1b[0m`);
204
- console.log(` \x1b[2mModel: ${modelLabel}\x1b[0m`);
205
- if (agentDef.tools && agentDef.tools.length > 0) {
206
- console.log(` \x1b[2mTools: ${agentDef.tools.join(", ")}\x1b[0m`);
207
- }
208
- console.log(` \x1b[2mPath: ${result.path}\x1b[0m`);
209
- console.log();
210
- console.log(`\x1b[2mRun an agent with: town run ${agentDef.name}\x1b[0m`);
211
- console.log(`\x1b[2mTUI mode (default), --gui for web interface, --http for API server\x1b[0m`);
212
- // Exit immediately
213
- process.exit(0);
134
+ // Exit after showing the done message
135
+ setTimeout(() => {
136
+ process.exit(0);
137
+ }, 100);
214
138
  }
215
139
  else {
216
140
  setScaffoldStatus("error");
@@ -271,7 +195,7 @@ function CreateApp({ name: initialName, model: initialModel, tools: initialTools
271
195
  setStage("systemPrompt");
272
196
  // Attempt to open editor
273
197
  setIsEditingPrompt(true);
274
- const editorContent = await openEditor(agentDef.systemPrompt || "You are a helpful assistant.");
198
+ const editorContent = await openInEditor(agentDef.systemPrompt || "You are a helpful assistant.");
275
199
  if (editorContent !== null) {
276
200
  // Editor worked
277
201
  setAgentDef({ ...agentDef, systemPrompt: editorContent });
@@ -363,13 +287,14 @@ function CreateApp({ name: initialName, model: initialModel, tools: initialTools
363
287
  // Done stage
364
288
  if (stage === "done") {
365
289
  if (scaffoldStatus === "scaffolding") {
366
- return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { children: "\u23F3 Creating agent package..." }) }));
290
+ return (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { children: ["\u23F3 Creating agent ", agentDef.name, "..."] }) }));
367
291
  }
368
292
  if (scaffoldStatus === "error") {
369
293
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", children: "\u2717 Error creating agent package" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: scaffoldError }) })] }));
370
294
  }
371
295
  if (scaffoldStatus === "done") {
372
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", children: "\u2713 Agent package created successfully!" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Path: ", agentPath] }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["You can run the agent with:", " ", _jsxs(Text, { bold: true, color: "cyan", children: ["town run ", agentDef.name] })] }) })] }));
296
+ const modelLabel = agentDef.model?.replace("claude-", "") || "";
297
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Agent created successfully!" }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: "green", children: "\u25CF" }), " ", _jsx(Text, { bold: true, children: agentDef.name })] }), _jsxs(Text, { dimColor: true, children: [" Model: ", modelLabel] }), agentDef.tools && agentDef.tools.length > 0 && (_jsxs(Text, { dimColor: true, children: [" Tools: ", agentDef.tools.join(", ")] })), _jsxs(Text, { dimColor: true, children: [" Path: ", agentPath] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["Run an agent with: town run ", agentDef.name] }), _jsx(Text, { dimColor: true, children: "TUI mode (default), --gui for web interface, --http for API server" })] })] }));
373
298
  }
374
299
  }
375
300
  return null;
@@ -1,9 +1,14 @@
1
1
  interface MCPAddProps {
2
- name?: string;
3
- url?: string;
4
- command?: string;
5
- args?: readonly string[];
2
+ name?: string;
3
+ url?: string;
4
+ command?: string;
5
+ args?: readonly string[];
6
6
  }
7
- declare function MCPAddApp({ name: initialName, url: initialUrl, command: initialCommand, args: initialArgs, }: MCPAddProps): import("react/jsx-runtime").JSX.Element | null;
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;
8
13
  export default MCPAddApp;
9
14
  export declare function runMCPAdd(props?: MCPAddProps): Promise<void>;
@@ -1,61 +1,188 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
1
  import { Box, render, Text, useApp, useInput } from "ink";
3
2
  import { useEffect, useState } from "react";
3
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  import { listMCPConfigs } from "../lib/mcp-storage";
5
+
5
6
  // ============================================================================
6
7
  // Main Component
7
8
  // ============================================================================
8
9
  function MCPListApp() {
9
- const [result, setResult] = useState(null);
10
- const [loading, setLoading] = useState(true);
11
- const { exit } = useApp();
12
- useEffect(() => {
13
- function loadConfigs() {
14
- try {
15
- const configs = listMCPConfigs();
16
- setResult({ configs });
17
- }
18
- catch (error) {
19
- const errorMsg = error instanceof Error ? error.message : String(error);
20
- setResult({ configs: [], error: errorMsg });
21
- }
22
- finally {
23
- setLoading(false);
24
- }
25
- }
26
- loadConfigs();
27
- }, []);
28
- // Exit on any key press when not loading
29
- useInput((_input, key) => {
30
- if (!loading) {
31
- if (key.return || key.escape || _input === "q") {
32
- exit();
33
- }
34
- }
35
- });
36
- if (loading) {
37
- return (_jsx(Box, { children: _jsx(Text, { children: "Loading MCP servers..." }) }));
38
- }
39
- if (result?.error) {
40
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "red", children: "\u274C Error loading MCP servers" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: result.error }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "Press Enter or Q to exit" }) })] }));
41
- }
42
- if (!result || result.configs.length === 0) {
43
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "No MCP servers configured" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Add one with: town mcp add" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "Press Enter or Q to exit" }) })] }));
44
- }
45
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, children: ["Configured MCP Servers (", result.configs.length, ")"] }) }), result.configs.map((config, index) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { children: _jsxs(Text, { bold: true, color: "cyan", children: [index + 1, ". ", config.name] }) }), _jsx(Box, { paddingLeft: 3, children: config.transport === "http" ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Transport: ", _jsx(Text, { color: "green", children: "HTTP" })] }), _jsxs(Text, { children: ["URL: ", config.url] })] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Transport: ", _jsx(Text, { color: "blue", children: "stdio" })] }), _jsxs(Text, { children: ["Command: ", config.command] }), config.args && config.args.length > 0 && (_jsxs(Text, { children: ["Args: ", config.args.join(" ")] }))] })) })] }, config.name))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Use `town mcp remove` to remove a server or `town mcp add` to add one" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter or Q to exit" }) })] }));
10
+ const [result, setResult] = useState(null);
11
+ const [loading, setLoading] = useState(true);
12
+ const { exit } = useApp();
13
+ useEffect(() => {
14
+ function loadConfigs() {
15
+ try {
16
+ const configs = listMCPConfigs();
17
+ setResult({ configs });
18
+ } catch (error) {
19
+ const errorMsg = error instanceof Error ? error.message : String(error);
20
+ setResult({ configs: [], error: errorMsg });
21
+ } finally {
22
+ setLoading(false);
23
+ }
24
+ }
25
+ loadConfigs();
26
+ }, []);
27
+ // Exit on any key press when not loading
28
+ useInput((_input, key) => {
29
+ if (!loading) {
30
+ if (key.return || key.escape || _input === "q") {
31
+ exit();
32
+ }
33
+ }
34
+ });
35
+ if (loading) {
36
+ return _jsx(Box, {
37
+ children: _jsx(Text, { children: "Loading MCP servers..." }),
38
+ });
39
+ }
40
+ if (result?.error) {
41
+ return _jsxs(Box, {
42
+ flexDirection: "column",
43
+ children: [
44
+ _jsx(Box, {
45
+ marginBottom: 1,
46
+ children: _jsx(Text, {
47
+ bold: true,
48
+ color: "red",
49
+ children: "\u274C Error loading MCP servers",
50
+ }),
51
+ }),
52
+ _jsx(Box, {
53
+ marginBottom: 1,
54
+ children: _jsx(Text, { children: result.error }),
55
+ }),
56
+ _jsx(Box, {
57
+ children: _jsx(Text, {
58
+ dimColor: true,
59
+ children: "Press Enter or Q to exit",
60
+ }),
61
+ }),
62
+ ],
63
+ });
64
+ }
65
+ if (!result || result.configs.length === 0) {
66
+ return _jsxs(Box, {
67
+ flexDirection: "column",
68
+ children: [
69
+ _jsx(Box, {
70
+ marginBottom: 1,
71
+ children: _jsx(Text, {
72
+ bold: true,
73
+ children: "No MCP servers configured",
74
+ }),
75
+ }),
76
+ _jsx(Box, {
77
+ marginBottom: 1,
78
+ children: _jsx(Text, {
79
+ dimColor: true,
80
+ children: "Add one with: town mcp add",
81
+ }),
82
+ }),
83
+ _jsx(Box, {
84
+ children: _jsx(Text, {
85
+ dimColor: true,
86
+ children: "Press Enter or Q to exit",
87
+ }),
88
+ }),
89
+ ],
90
+ });
91
+ }
92
+ return _jsxs(Box, {
93
+ flexDirection: "column",
94
+ children: [
95
+ _jsx(Box, {
96
+ marginBottom: 1,
97
+ children: _jsxs(Text, {
98
+ bold: true,
99
+ children: ["Configured MCP Servers (", result.configs.length, ")"],
100
+ }),
101
+ }),
102
+ result.configs.map((config, index) =>
103
+ _jsxs(
104
+ Box,
105
+ {
106
+ flexDirection: "column",
107
+ marginBottom: 1,
108
+ children: [
109
+ _jsx(Box, {
110
+ children: _jsxs(Text, {
111
+ bold: true,
112
+ color: "cyan",
113
+ children: [index + 1, ". ", config.name],
114
+ }),
115
+ }),
116
+ _jsx(Box, {
117
+ paddingLeft: 3,
118
+ children:
119
+ config.transport === "http"
120
+ ? _jsxs(Box, {
121
+ flexDirection: "column",
122
+ children: [
123
+ _jsxs(Text, {
124
+ children: [
125
+ "Transport: ",
126
+ _jsx(Text, { color: "green", children: "HTTP" }),
127
+ ],
128
+ }),
129
+ _jsxs(Text, { children: ["URL: ", config.url] }),
130
+ ],
131
+ })
132
+ : _jsxs(Box, {
133
+ flexDirection: "column",
134
+ children: [
135
+ _jsxs(Text, {
136
+ children: [
137
+ "Transport: ",
138
+ _jsx(Text, { color: "blue", children: "stdio" }),
139
+ ],
140
+ }),
141
+ _jsxs(Text, {
142
+ children: ["Command: ", config.command],
143
+ }),
144
+ config.args &&
145
+ config.args.length > 0 &&
146
+ _jsxs(Text, {
147
+ children: ["Args: ", config.args.join(" ")],
148
+ }),
149
+ ],
150
+ }),
151
+ }),
152
+ ],
153
+ },
154
+ config.name,
155
+ ),
156
+ ),
157
+ _jsx(Box, {
158
+ marginTop: 1,
159
+ children: _jsx(Text, {
160
+ dimColor: true,
161
+ children:
162
+ "Use `town mcp remove` to remove a server or `town mcp add` to add one",
163
+ }),
164
+ }),
165
+ _jsx(Box, {
166
+ marginTop: 1,
167
+ children: _jsx(Text, {
168
+ dimColor: true,
169
+ children: "Press Enter or Q to exit",
170
+ }),
171
+ }),
172
+ ],
173
+ });
46
174
  }
47
175
  // ============================================================================
48
176
  // Export and Runner
49
177
  // ============================================================================
50
178
  export default MCPListApp;
51
179
  export async function runMCPList() {
52
- const { waitUntilExit, clear } = render(_jsx(MCPListApp, {}));
53
- try {
54
- await waitUntilExit();
55
- }
56
- finally {
57
- clear();
58
- // Ensure cursor is visible
59
- process.stdout.write("\x1B[?25h");
60
- }
180
+ const { waitUntilExit, clear } = render(_jsx(MCPListApp, {}));
181
+ try {
182
+ await waitUntilExit();
183
+ } finally {
184
+ clear();
185
+ // Ensure cursor is visible
186
+ process.stdout.write("\x1B[?25h");
187
+ }
61
188
  }
@@ -1,10 +1,42 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
1
2
  import { spawn } from "node:child_process";
2
3
  import { existsSync } from "node:fs";
3
4
  import { readFile } from "node:fs/promises";
4
5
  import { homedir } from "node:os";
5
6
  import { join } from "node:path";
6
7
  import { agentExists, getAgentPath } from "@townco/agent/storage";
8
+ import { render } from "ink";
7
9
  import open from "open";
10
+ import { useCallback, useMemo, useRef, useState } from "react";
11
+ import { TabbedOutput } from "../components/TabbedOutput.js";
12
+ import { findAvailablePort } from "../lib/port-utils.js";
13
+ function GuiRunner({ agentProcess, guiProcess, agentPort, onExit, }) {
14
+ const [guiPort, setGuiPort] = useState(5173);
15
+ const browserOpenedRef = useRef(false);
16
+ const handlePortDetected = useCallback((processIndex, port) => {
17
+ // Process index 1 is the GUI process
18
+ if (processIndex === 1) {
19
+ setGuiPort(port);
20
+ // Open browser once we know the actual port
21
+ if (!browserOpenedRef.current) {
22
+ browserOpenedRef.current = true;
23
+ const guiUrl = `http://localhost:${port}`;
24
+ open(guiUrl).catch((error) => {
25
+ // Silently fail - user can open manually
26
+ console.warn(`Could not automatically open browser: ${error.message}`);
27
+ });
28
+ }
29
+ }
30
+ }, []);
31
+ // Memoize processes array based only on actual process objects and initial ports
32
+ // Don't include guiPort as dependency to prevent re-creating array when port is detected
33
+ // TabbedOutput will update the displayed port internally via onPortDetected callback
34
+ const processes = useMemo(() => [
35
+ { name: "Agent", process: agentProcess, port: agentPort },
36
+ { name: "GUI", process: guiProcess, port: guiPort },
37
+ ], [agentProcess, guiProcess, agentPort, guiPort]);
38
+ return (_jsx(TabbedOutput, { processes: processes, onExit: onExit, onPortDetected: handlePortDetected }));
39
+ }
8
40
  async function loadEnvVars() {
9
41
  const envPath = join(homedir(), ".config", "town", ".env");
10
42
  const envVars = {};
@@ -42,7 +74,7 @@ export async function runCommand(options) {
42
74
  }
43
75
  const agentPath = getAgentPath(name);
44
76
  const binPath = join(agentPath, "bin.ts");
45
- // If GUI or HTTP-only mode, run differently
77
+ // If GUI mode, run with tabbed interface
46
78
  if (gui) {
47
79
  const guiPath = join(agentPath, "gui");
48
80
  // Check if GUI exists
@@ -56,72 +88,58 @@ export async function runCommand(options) {
56
88
  console.log(`Recreate the agent with "town create" to include the GUI.`);
57
89
  process.exit(1);
58
90
  }
91
+ // Find an available port for the agent
92
+ const availablePort = await findAvailablePort(port);
93
+ if (availablePort !== port) {
94
+ console.log(`Port ${port} is in use, using port ${availablePort} instead`);
95
+ }
59
96
  console.log(`Starting agent "${name}" with GUI...`);
60
- console.log(`Starting agent HTTP server on port ${port}...`);
61
- console.log(`Starting GUI development server...\n`);
62
- // Start the agent in HTTP mode first
97
+ console.log(`Agent HTTP server will run on port ${availablePort}`);
98
+ console.log(`GUI dev server will run on port 5173\n`);
99
+ // Set stdin to raw mode for Ink
100
+ if (process.stdin.isTTY) {
101
+ process.stdin.setRawMode(true);
102
+ }
103
+ // Start the agent in HTTP mode
63
104
  const agentProcess = spawn("bun", [binPath, "http"], {
64
105
  cwd: agentPath,
65
- stdio: "pipe",
106
+ stdio: ["ignore", "pipe", "pipe"], // Pipe stdout/stderr for capture
66
107
  env: {
67
108
  ...process.env,
68
109
  ...configEnvVars,
69
110
  NODE_ENV: process.env.NODE_ENV || "production",
70
- PORT: port.toString(),
111
+ PORT: availablePort.toString(),
71
112
  },
72
113
  });
73
- agentProcess.on("error", (error) => {
74
- console.error(`Failed to start agent: ${error.message}`);
75
- process.exit(1);
114
+ // Start the GUI dev server
115
+ const guiProcess = spawn("bun", ["run", "dev"], {
116
+ cwd: guiPath,
117
+ stdio: ["ignore", "pipe", "pipe"], // Pipe stdout/stderr for capture
118
+ env: {
119
+ ...process.env,
120
+ ...configEnvVars,
121
+ VITE_AGENT_URL: `http://localhost:${availablePort}`,
122
+ },
76
123
  });
77
- // Wait a bit for agent to start, then start the GUI
78
- setTimeout(() => {
79
- // Start the GUI dev server
80
- const guiProcess = spawn("bun", ["run", "dev"], {
81
- cwd: guiPath,
82
- stdio: "inherit",
83
- env: {
84
- ...process.env,
85
- ...configEnvVars,
86
- VITE_AGENT_URL: `http://localhost:${port}`,
87
- },
88
- });
89
- guiProcess.on("error", (error) => {
90
- console.error(`Failed to start GUI: ${error.message}`);
91
- agentProcess.kill();
92
- process.exit(1);
93
- });
94
- guiProcess.on("close", (code) => {
124
+ // Render the tabbed UI with dynamic port detection
125
+ const { waitUntilExit } = render(_jsx(GuiRunner, { agentProcess: agentProcess, guiProcess: guiProcess, agentPort: availablePort, onExit: () => {
95
126
  agentProcess.kill();
96
- if (code !== 0 && code !== null) {
97
- console.error(`GUI exited with code ${code}`);
98
- process.exit(code);
99
- }
100
- });
101
- // Open browser after GUI server has time to start (default Vite port is 5173)
102
- setTimeout(() => {
103
- const guiUrl = "http://localhost:5173";
104
- console.log(`Opening browser at ${guiUrl}...`);
105
- open(guiUrl).catch((error) => {
106
- console.warn(`Could not automatically open browser: ${error.message}`);
107
- console.log(`Please manually open: ${guiUrl}`);
108
- });
109
- }, 2000);
110
- }, 1000);
111
- agentProcess.on("close", (code) => {
112
- if (code !== 0 && code !== null) {
113
- console.error(`Agent exited with code ${code}`);
114
- process.exit(code);
115
- }
116
- });
117
- return;
127
+ guiProcess.kill();
128
+ } }));
129
+ await waitUntilExit();
130
+ process.exit(0);
118
131
  }
119
132
  else if (http) {
120
- console.log(`Starting agent "${name}" in HTTP mode on port ${port}...`);
133
+ // Find an available port for the agent
134
+ const availablePort = await findAvailablePort(port);
135
+ if (availablePort !== port) {
136
+ console.log(`Port ${port} is in use, using port ${availablePort} instead\n`);
137
+ }
138
+ console.log(`Starting agent "${name}" in HTTP mode on port ${availablePort}...`);
121
139
  console.log(`\nEndpoints:`);
122
- console.log(` http://localhost:${port}/health - Health check`);
123
- console.log(` http://localhost:${port}/rpc - RPC endpoint`);
124
- console.log(` http://localhost:${port}/events - SSE event stream\n`);
140
+ console.log(` http://localhost:${availablePort}/health - Health check`);
141
+ console.log(` http://localhost:${availablePort}/rpc - RPC endpoint`);
142
+ console.log(` http://localhost:${availablePort}/events - SSE event stream\n`);
125
143
  // Run the agent in HTTP mode
126
144
  const agentProcess = spawn("bun", [binPath, "http"], {
127
145
  cwd: agentPath,
@@ -130,7 +148,7 @@ export async function runCommand(options) {
130
148
  ...process.env,
131
149
  ...configEnvVars,
132
150
  NODE_ENV: process.env.NODE_ENV || "production",
133
- PORT: port.toString(),
151
+ PORT: availablePort.toString(),
134
152
  },
135
153
  });
136
154
  agentProcess.on("error", (error) => {