@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.
- package/dist/commands/create.d.ts +5 -6
- package/dist/commands/create.js +19 -94
- package/dist/commands/mcp-add.d.ts +10 -5
- package/dist/commands/mcp-list.js +174 -47
- package/dist/commands/run.js +72 -54
- package/dist/commands/tool-add.js +2 -29
- package/dist/components/ProcessPane.d.ts +7 -0
- package/dist/components/ProcessPane.js +17 -0
- package/dist/components/StatusLine.d.ts +5 -0
- package/dist/components/StatusLine.js +5 -0
- package/dist/components/TabbedOutput.d.ts +12 -0
- package/dist/components/TabbedOutput.js +136 -0
- package/dist/lib/editor-utils.d.ts +15 -0
- package/dist/lib/editor-utils.js +112 -0
- package/dist/lib/mcp-storage.d.ts +5 -5
- package/dist/lib/mcp-storage.js +50 -48
- package/dist/lib/port-utils.d.ts +8 -0
- package/dist/lib/port-utils.js +35 -0
- package/package.json +5 -5
- package/dist/commands/tool-stub.d.ts +0 -6
- package/dist/commands/tool-stub.js +0 -376
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
interface CreateCommandProps {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 {};
|
package/dist/commands/create.js
CHANGED
|
@@ -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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
name?: string;
|
|
3
|
+
url?: string;
|
|
4
|
+
command?: string;
|
|
5
|
+
args?: readonly string[];
|
|
6
6
|
}
|
|
7
|
-
declare function MCPAddApp({
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -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
|
|
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(`
|
|
61
|
-
console.log(`
|
|
62
|
-
//
|
|
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:
|
|
111
|
+
PORT: availablePort.toString(),
|
|
71
112
|
},
|
|
72
113
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
//
|
|
78
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
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:${
|
|
123
|
-
console.log(` http://localhost:${
|
|
124
|
-
console.log(` http://localhost:${
|
|
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:
|
|
151
|
+
PORT: availablePort.toString(),
|
|
134
152
|
},
|
|
135
153
|
});
|
|
136
154
|
agentProcess.on("error", (error) => {
|