@townco/cli 0.1.13 → 0.1.15
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 +7 -16
- package/dist/commands/mcp-add.d.ts +10 -5
- package/dist/commands/mcp-add.js +54 -5
- package/dist/commands/mcp-list.js +3 -1
- package/dist/commands/run.js +70 -54
- 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/index.js +0 -0
- package/dist/lib/mcp-storage.d.ts +1 -0
- package/dist/lib/mcp-storage.js +5 -1
- package/dist/lib/port-utils.d.ts +8 -0
- package/dist/lib/port-utils.js +35 -0
- package/package.json +5 -5
|
@@ -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
|
@@ -131,20 +131,10 @@ function CreateApp({ name: initialName, model: initialModel, tools: initialTools
|
|
|
131
131
|
if (result.success) {
|
|
132
132
|
setScaffoldStatus("done");
|
|
133
133
|
setAgentPath(result.path);
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
console.log(` \x1b[2mModel: ${modelLabel}\x1b[0m`);
|
|
139
|
-
if (agentDef.tools && agentDef.tools.length > 0) {
|
|
140
|
-
console.log(` \x1b[2mTools: ${agentDef.tools.join(", ")}\x1b[0m`);
|
|
141
|
-
}
|
|
142
|
-
console.log(` \x1b[2mPath: ${result.path}\x1b[0m`);
|
|
143
|
-
console.log();
|
|
144
|
-
console.log(`\x1b[2mRun an agent with: town run ${agentDef.name}\x1b[0m`);
|
|
145
|
-
console.log(`\x1b[2mTUI mode (default), --gui for web interface, --http for API server\x1b[0m`);
|
|
146
|
-
// Exit immediately
|
|
147
|
-
process.exit(0);
|
|
134
|
+
// Exit after showing the done message
|
|
135
|
+
setTimeout(() => {
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}, 100);
|
|
148
138
|
}
|
|
149
139
|
else {
|
|
150
140
|
setScaffoldStatus("error");
|
|
@@ -297,13 +287,14 @@ function CreateApp({ name: initialName, model: initialModel, tools: initialTools
|
|
|
297
287
|
// Done stage
|
|
298
288
|
if (stage === "done") {
|
|
299
289
|
if (scaffoldStatus === "scaffolding") {
|
|
300
|
-
return (_jsx(Box, { flexDirection: "column", children:
|
|
290
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { children: ["\u23F3 Creating agent ", agentDef.name, "..."] }) }));
|
|
301
291
|
}
|
|
302
292
|
if (scaffoldStatus === "error") {
|
|
303
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 }) })] }));
|
|
304
294
|
}
|
|
305
295
|
if (scaffoldStatus === "done") {
|
|
306
|
-
|
|
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" })] })] }));
|
|
307
298
|
}
|
|
308
299
|
}
|
|
309
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>;
|
package/dist/commands/mcp-add.js
CHANGED
|
@@ -102,6 +102,34 @@ function HttpUrlStage({ serverName, value, onChange, onNext, onBack, onError, })
|
|
|
102
102
|
};
|
|
103
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
104
|
}
|
|
105
|
+
function HttpHeadersStage({ serverName, headers, onAddHeader, onNext, onBack, }) {
|
|
106
|
+
const [headerKeyInput, setHeaderKeyInput] = useState("");
|
|
107
|
+
const [headerValueInput, setHeaderValueInput] = useState("");
|
|
108
|
+
const [inputStage, setInputStage] = useState("key");
|
|
109
|
+
const headerEntries = Object.entries(headers);
|
|
110
|
+
useInput((_input, key) => {
|
|
111
|
+
if (key.escape && inputStage === "key") {
|
|
112
|
+
onBack();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, children: ["Add HTTP headers for: ", serverName, " (", headerEntries.length, " added)"] }) }), headerEntries.length > 0 && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Current headers:" }), headerEntries.map(([key, value], index) => (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { children: [index + 1, ". ", key, ": ", value] }) }, key)))] })), inputStage === "key" && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { children: [">", " Header name: "] }), _jsx(TextInput, { value: headerKeyInput, onChange: setHeaderKeyInput, onSubmit: () => {
|
|
116
|
+
const trimmed = headerKeyInput.trim();
|
|
117
|
+
if (trimmed) {
|
|
118
|
+
setInputStage("value");
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
onNext();
|
|
122
|
+
}
|
|
123
|
+
}, placeholder: "Enter header name (or leave empty to continue)" })] }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Enter: Submit header name or continue \u2022 Esc: Back" }) })] })), inputStage === "value" && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { children: [">", " Value for ", headerKeyInput, ":", " "] }), _jsx(TextInput, { value: headerValueInput, onChange: setHeaderValueInput, onSubmit: () => {
|
|
124
|
+
const trimmedValue = headerValueInput.trim();
|
|
125
|
+
if (trimmedValue) {
|
|
126
|
+
onAddHeader(headerKeyInput, trimmedValue);
|
|
127
|
+
setHeaderKeyInput("");
|
|
128
|
+
setHeaderValueInput("");
|
|
129
|
+
setInputStage("key");
|
|
130
|
+
}
|
|
131
|
+
}, placeholder: "Header value" })] }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Enter: Add header" }) })] }))] }));
|
|
132
|
+
}
|
|
105
133
|
function ReviewStage({ config, onEdit, onSave, onBack }) {
|
|
106
134
|
const [reviewSelection, setReviewSelection] = useState(null);
|
|
107
135
|
const reviewOptions = [
|
|
@@ -137,9 +165,13 @@ function ReviewStage({ config, onEdit, onSave, onBack }) {
|
|
|
137
165
|
label: "Edit URL",
|
|
138
166
|
value: "httpUrl",
|
|
139
167
|
description: "Change the HTTP URL",
|
|
168
|
+
}, {
|
|
169
|
+
label: "Edit headers",
|
|
170
|
+
value: "httpHeaders",
|
|
171
|
+
description: "Change the HTTP headers",
|
|
140
172
|
});
|
|
141
173
|
}
|
|
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) => {
|
|
174
|
+
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(_Fragment, { children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "URL: " }), config.url] }), config.headers && Object.keys(config.headers).length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Headers:" }), Object.entries(config.headers).map(([key, value], index) => (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { dimColor: true, children: [index + 1, ". ", key, ": ", value] }) }, key)))] }))] }))] }), _jsx(SingleSelect, { options: reviewOptions, selected: reviewSelection, onChange: setReviewSelection, onSubmit: (value) => {
|
|
143
175
|
setReviewSelection(value);
|
|
144
176
|
if (value === "continue") {
|
|
145
177
|
onSave();
|
|
@@ -148,7 +180,8 @@ function ReviewStage({ config, onEdit, onSave, onBack }) {
|
|
|
148
180
|
value === "transport" ||
|
|
149
181
|
value === "stdioCommand" ||
|
|
150
182
|
value === "stdioArgs" ||
|
|
151
|
-
value === "httpUrl"
|
|
183
|
+
value === "httpUrl" ||
|
|
184
|
+
value === "httpHeaders") {
|
|
152
185
|
onEdit(value);
|
|
153
186
|
}
|
|
154
187
|
}, onCancel: onBack })] }));
|
|
@@ -228,7 +261,7 @@ function MCPAddApp({ name: initialName, url: initialUrl, command: initialCommand
|
|
|
228
261
|
}
|
|
229
262
|
// If URL provided, transport is http
|
|
230
263
|
if (initialUrl) {
|
|
231
|
-
return initialName ? "
|
|
264
|
+
return initialName ? "httpHeaders" : "name";
|
|
232
265
|
}
|
|
233
266
|
return "transport";
|
|
234
267
|
};
|
|
@@ -278,6 +311,7 @@ function MCPAddApp({ name: initialName, url: initialUrl, command: initialCommand
|
|
|
278
311
|
}),
|
|
279
312
|
...(config.transport === "http" && {
|
|
280
313
|
url: config.url,
|
|
314
|
+
...(config.headers && { headers: config.headers }),
|
|
281
315
|
}),
|
|
282
316
|
};
|
|
283
317
|
// Save to global MCP storage
|
|
@@ -390,8 +424,11 @@ function MCPAddApp({ name: initialName, url: initialUrl, command: initialCommand
|
|
|
390
424
|
setConfig({ ...config, url });
|
|
391
425
|
if (isEditingFromReview) {
|
|
392
426
|
setIsEditingFromReview(false);
|
|
427
|
+
setStage("review");
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
setStage("httpHeaders");
|
|
393
431
|
}
|
|
394
|
-
setStage("review");
|
|
395
432
|
}, onBack: () => {
|
|
396
433
|
if (initialUrl) {
|
|
397
434
|
exit();
|
|
@@ -401,6 +438,18 @@ function MCPAddApp({ name: initialName, url: initialUrl, command: initialCommand
|
|
|
401
438
|
}
|
|
402
439
|
}, onError: setSaveError }));
|
|
403
440
|
}
|
|
441
|
+
// HTTP Headers input stage
|
|
442
|
+
if (stage === "httpHeaders") {
|
|
443
|
+
return (_jsx(HttpHeadersStage, { serverName: config.name || "", headers: config.headers || {}, onAddHeader: (key, value) => setConfig({
|
|
444
|
+
...config,
|
|
445
|
+
headers: { ...(config.headers || {}), [key]: value },
|
|
446
|
+
}), onNext: () => {
|
|
447
|
+
if (isEditingFromReview) {
|
|
448
|
+
setIsEditingFromReview(false);
|
|
449
|
+
}
|
|
450
|
+
setStage("review");
|
|
451
|
+
}, onBack: () => setStage("httpUrl") }));
|
|
452
|
+
}
|
|
404
453
|
// Review stage
|
|
405
454
|
if (stage === "review") {
|
|
406
455
|
return (_jsx(ReviewStage, { config: config, onEdit: (editStage) => {
|
|
@@ -413,7 +462,7 @@ function MCPAddApp({ name: initialName, url: initialUrl, command: initialCommand
|
|
|
413
462
|
setStage("stdioArgs");
|
|
414
463
|
}
|
|
415
464
|
else {
|
|
416
|
-
setStage("
|
|
465
|
+
setStage("httpHeaders");
|
|
417
466
|
}
|
|
418
467
|
} }));
|
|
419
468
|
}
|
|
@@ -42,7 +42,9 @@ function MCPListApp() {
|
|
|
42
42
|
if (!result || result.configs.length === 0) {
|
|
43
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
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] })
|
|
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] }), config.headers && Object.keys(config.headers).length > 0 && (_jsxs(Text, { children: ["Headers:", " ", Object.entries(config.headers)
|
|
46
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
47
|
+
.join(", ")] }))] })) : (_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" }) })] }));
|
|
46
48
|
}
|
|
47
49
|
// ============================================================================
|
|
48
50
|
// Export and Runner
|
package/dist/commands/run.js
CHANGED
|
@@ -1,10 +1,40 @@
|
|
|
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 } 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 browserOpenedRef = useRef(false);
|
|
15
|
+
const handlePortDetected = useCallback((processIndex, port) => {
|
|
16
|
+
// Process index 1 is the GUI process
|
|
17
|
+
if (processIndex === 1) {
|
|
18
|
+
// Open browser once we know the actual port
|
|
19
|
+
if (!browserOpenedRef.current) {
|
|
20
|
+
browserOpenedRef.current = true;
|
|
21
|
+
const guiUrl = `http://localhost:${port}`;
|
|
22
|
+
open(guiUrl).catch((error) => {
|
|
23
|
+
// Silently fail - user can open manually
|
|
24
|
+
console.warn(`Could not automatically open browser: ${error.message}`);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}, []);
|
|
29
|
+
// Memoize processes array based only on actual process objects and initial ports
|
|
30
|
+
// Don't include guiPort as dependency to prevent re-creating array when port is detected
|
|
31
|
+
// TabbedOutput will update the displayed port internally via onPortDetected callback
|
|
32
|
+
const processes = useMemo(() => [
|
|
33
|
+
{ name: "Agent", process: agentProcess, port: agentPort },
|
|
34
|
+
{ name: "GUI", process: guiProcess }, // Port will be detected dynamically
|
|
35
|
+
], [agentProcess, guiProcess, agentPort]);
|
|
36
|
+
return (_jsx(TabbedOutput, { processes: processes, onExit: onExit, onPortDetected: handlePortDetected }));
|
|
37
|
+
}
|
|
8
38
|
async function loadEnvVars() {
|
|
9
39
|
const envPath = join(homedir(), ".config", "town", ".env");
|
|
10
40
|
const envVars = {};
|
|
@@ -42,7 +72,7 @@ export async function runCommand(options) {
|
|
|
42
72
|
}
|
|
43
73
|
const agentPath = getAgentPath(name);
|
|
44
74
|
const binPath = join(agentPath, "bin.ts");
|
|
45
|
-
// If GUI
|
|
75
|
+
// If GUI mode, run with tabbed interface
|
|
46
76
|
if (gui) {
|
|
47
77
|
const guiPath = join(agentPath, "gui");
|
|
48
78
|
// Check if GUI exists
|
|
@@ -56,72 +86,58 @@ export async function runCommand(options) {
|
|
|
56
86
|
console.log(`Recreate the agent with "town create" to include the GUI.`);
|
|
57
87
|
process.exit(1);
|
|
58
88
|
}
|
|
89
|
+
// Find an available port for the agent
|
|
90
|
+
const availablePort = await findAvailablePort(port);
|
|
91
|
+
if (availablePort !== port) {
|
|
92
|
+
console.log(`Port ${port} is in use, using port ${availablePort} instead`);
|
|
93
|
+
}
|
|
59
94
|
console.log(`Starting agent "${name}" with GUI...`);
|
|
60
|
-
console.log(`
|
|
61
|
-
console.log(`
|
|
62
|
-
//
|
|
95
|
+
console.log(`Agent HTTP server will run on port ${availablePort}`);
|
|
96
|
+
console.log(`GUI dev server will run on port 5173\n`);
|
|
97
|
+
// Set stdin to raw mode for Ink
|
|
98
|
+
if (process.stdin.isTTY) {
|
|
99
|
+
process.stdin.setRawMode(true);
|
|
100
|
+
}
|
|
101
|
+
// Start the agent in HTTP mode
|
|
63
102
|
const agentProcess = spawn("bun", [binPath, "http"], {
|
|
64
103
|
cwd: agentPath,
|
|
65
|
-
stdio: "pipe",
|
|
104
|
+
stdio: ["ignore", "pipe", "pipe"], // Pipe stdout/stderr for capture
|
|
66
105
|
env: {
|
|
67
106
|
...process.env,
|
|
68
107
|
...configEnvVars,
|
|
69
108
|
NODE_ENV: process.env.NODE_ENV || "production",
|
|
70
|
-
PORT:
|
|
109
|
+
PORT: availablePort.toString(),
|
|
71
110
|
},
|
|
72
111
|
});
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
112
|
+
// Start the GUI dev server
|
|
113
|
+
const guiProcess = spawn("bun", ["run", "dev"], {
|
|
114
|
+
cwd: guiPath,
|
|
115
|
+
stdio: ["ignore", "pipe", "pipe"], // Pipe stdout/stderr for capture
|
|
116
|
+
env: {
|
|
117
|
+
...process.env,
|
|
118
|
+
...configEnvVars,
|
|
119
|
+
VITE_AGENT_URL: `http://localhost:${availablePort}`,
|
|
120
|
+
},
|
|
76
121
|
});
|
|
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) => {
|
|
122
|
+
// Render the tabbed UI with dynamic port detection
|
|
123
|
+
const { waitUntilExit } = render(_jsx(GuiRunner, { agentProcess: agentProcess, guiProcess: guiProcess, agentPort: availablePort, onExit: () => {
|
|
95
124
|
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;
|
|
125
|
+
guiProcess.kill();
|
|
126
|
+
} }));
|
|
127
|
+
await waitUntilExit();
|
|
128
|
+
process.exit(0);
|
|
118
129
|
}
|
|
119
130
|
else if (http) {
|
|
120
|
-
|
|
131
|
+
// Find an available port for the agent
|
|
132
|
+
const availablePort = await findAvailablePort(port);
|
|
133
|
+
if (availablePort !== port) {
|
|
134
|
+
console.log(`Port ${port} is in use, using port ${availablePort} instead\n`);
|
|
135
|
+
}
|
|
136
|
+
console.log(`Starting agent "${name}" in HTTP mode on port ${availablePort}...`);
|
|
121
137
|
console.log(`\nEndpoints:`);
|
|
122
|
-
console.log(` http://localhost:${
|
|
123
|
-
console.log(` http://localhost:${
|
|
124
|
-
console.log(` http://localhost:${
|
|
138
|
+
console.log(` http://localhost:${availablePort}/health - Health check`);
|
|
139
|
+
console.log(` http://localhost:${availablePort}/rpc - RPC endpoint`);
|
|
140
|
+
console.log(` http://localhost:${availablePort}/events - SSE event stream\n`);
|
|
125
141
|
// Run the agent in HTTP mode
|
|
126
142
|
const agentProcess = spawn("bun", [binPath, "http"], {
|
|
127
143
|
cwd: agentPath,
|
|
@@ -130,7 +146,7 @@ export async function runCommand(options) {
|
|
|
130
146
|
...process.env,
|
|
131
147
|
...configEnvVars,
|
|
132
148
|
NODE_ENV: process.env.NODE_ENV || "production",
|
|
133
|
-
PORT:
|
|
149
|
+
PORT: availablePort.toString(),
|
|
134
150
|
},
|
|
135
151
|
});
|
|
136
152
|
agentProcess.on("error", (error) => {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface ProcessPaneProps {
|
|
2
|
+
title: string;
|
|
3
|
+
output: string[];
|
|
4
|
+
port: number | undefined;
|
|
5
|
+
status: "starting" | "running" | "stopped" | "error";
|
|
6
|
+
}
|
|
7
|
+
export declare function ProcessPane({ title, output, port, status }: ProcessPaneProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
export function ProcessPane({ title, output, port, status }) {
|
|
5
|
+
// Keep only last 50 lines to prevent memory issues
|
|
6
|
+
const displayOutput = useMemo(() => {
|
|
7
|
+
return output.slice(-50);
|
|
8
|
+
}, [output]);
|
|
9
|
+
const statusColor = status === "running"
|
|
10
|
+
? "green"
|
|
11
|
+
: status === "error"
|
|
12
|
+
? "red"
|
|
13
|
+
: status === "starting"
|
|
14
|
+
? "yellow"
|
|
15
|
+
: "gray";
|
|
16
|
+
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsxs(Box, { borderStyle: "single", paddingX: 1, marginBottom: 1, flexShrink: 0, children: [_jsx(Text, { color: "cyan", bold: true, children: title }), port && _jsxs(Text, { color: "gray", children: [" - http://localhost:", port] }), _jsx(Text, { children: " " }), _jsx(Text, { color: statusColor, children: "\u25CF" }), _jsxs(Text, { children: [" ", status] })] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, paddingX: 1, children: displayOutput.length === 0 ? (_jsx(Text, { color: "gray", children: "Waiting for output..." })) : (displayOutput.map((line, idx) => (_jsx(Text, { children: line }, `${idx}-${line.slice(0, 20)}`)))) }), _jsx(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexShrink: 0, children: _jsxs(Text, { color: "gray", children: ["Lines: ", output.length, " | Displaying last ", displayOutput.length] }) })] }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
export function StatusLine({ activeTab, tabs }) {
|
|
4
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: "blue", paddingX: 1, children: [tabs.map((tab, idx) => (_jsxs(Text, { children: [_jsxs(Text, { color: activeTab === idx ? "green" : "gray", children: [idx + 1, ". ", tab] }), idx < tabs.length - 1 && _jsx(Text, { color: "gray", children: " | " })] }, tab))), _jsx(Text, { color: "gray", children: " | " }), _jsx(Text, { color: "yellow", children: "tab" }), _jsx(Text, { color: "gray", children: ", " }), _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), _jsx(Text, { color: "gray", children: " or " }), _jsxs(Text, { color: "yellow", children: ["1-", tabs.length] }), _jsx(Text, { color: "gray", children: " to switch" })] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
export interface ProcessInfo {
|
|
3
|
+
name: string;
|
|
4
|
+
process: ChildProcess;
|
|
5
|
+
port?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface TabbedOutputProps {
|
|
8
|
+
processes: ProcessInfo[];
|
|
9
|
+
onExit: () => void;
|
|
10
|
+
onPortDetected?: (processIndex: number, port: number) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function TabbedOutput({ processes, onExit, onPortDetected, }: TabbedOutputProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, useApp, useInput } from "ink";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
import { ProcessPane } from "./ProcessPane.js";
|
|
5
|
+
import { StatusLine } from "./StatusLine.js";
|
|
6
|
+
export function TabbedOutput({ processes, onExit, onPortDetected, }) {
|
|
7
|
+
const { exit } = useApp();
|
|
8
|
+
const [activeTab, setActiveTab] = useState(0);
|
|
9
|
+
const [outputs, setOutputs] = useState(processes.map(() => []));
|
|
10
|
+
const [statuses, setStatuses] = useState(processes.map(() => "starting"));
|
|
11
|
+
const [ports, setPorts] = useState(processes.map((p) => p.port));
|
|
12
|
+
const portDetectedRef = useRef(new Set());
|
|
13
|
+
// Handle keyboard input
|
|
14
|
+
useInput((input, key) => {
|
|
15
|
+
if (key.ctrl && input === "c") {
|
|
16
|
+
// Cleanup and exit
|
|
17
|
+
onExit();
|
|
18
|
+
exit();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
// Tab key to toggle between tabs
|
|
22
|
+
if (key.tab) {
|
|
23
|
+
setActiveTab((activeTab + 1) % processes.length);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (key.leftArrow && activeTab > 0) {
|
|
27
|
+
setActiveTab(activeTab - 1);
|
|
28
|
+
}
|
|
29
|
+
else if (key.rightArrow && activeTab < processes.length - 1) {
|
|
30
|
+
setActiveTab(activeTab + 1);
|
|
31
|
+
}
|
|
32
|
+
// Number keys to jump to tabs
|
|
33
|
+
const num = Number.parseInt(input, 10);
|
|
34
|
+
if (!Number.isNaN(num) && num >= 1 && num <= processes.length) {
|
|
35
|
+
setActiveTab(num - 1);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
// Set up process output listeners
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
processes.forEach((processInfo, idx) => {
|
|
41
|
+
const { process } = processInfo;
|
|
42
|
+
// Mark as running when we see first output
|
|
43
|
+
const markAsRunning = () => {
|
|
44
|
+
setStatuses((prev) => {
|
|
45
|
+
const newStatuses = [...prev];
|
|
46
|
+
if (newStatuses[idx] === "starting") {
|
|
47
|
+
newStatuses[idx] = "running";
|
|
48
|
+
}
|
|
49
|
+
return newStatuses;
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
// Capture stdout
|
|
53
|
+
process.stdout?.on("data", (data) => {
|
|
54
|
+
const output = data.toString();
|
|
55
|
+
const lines = output.split("\n").filter(Boolean);
|
|
56
|
+
setOutputs((prev) => {
|
|
57
|
+
const newOutputs = [...prev];
|
|
58
|
+
newOutputs[idx] = [...(newOutputs[idx] || []), ...lines];
|
|
59
|
+
return newOutputs;
|
|
60
|
+
});
|
|
61
|
+
markAsRunning();
|
|
62
|
+
// Check for Vite port in output and notify if callback provided
|
|
63
|
+
if (onPortDetected && !portDetectedRef.current.has(idx)) {
|
|
64
|
+
const portMatch = output.match(/Local:\s+http:\/\/localhost:(\d+)/);
|
|
65
|
+
if (portMatch?.[1]) {
|
|
66
|
+
const detectedPort = Number.parseInt(portMatch[1], 10);
|
|
67
|
+
portDetectedRef.current.add(idx);
|
|
68
|
+
// Update our internal ports state
|
|
69
|
+
setPorts((prev) => {
|
|
70
|
+
const newPorts = [...prev];
|
|
71
|
+
newPorts[idx] = detectedPort;
|
|
72
|
+
return newPorts;
|
|
73
|
+
});
|
|
74
|
+
onPortDetected(idx, detectedPort);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
// Capture stderr
|
|
79
|
+
process.stderr?.on("data", (data) => {
|
|
80
|
+
const text = data.toString();
|
|
81
|
+
const lines = text.split("\n").filter(Boolean);
|
|
82
|
+
const errorLines = lines.map((line) => `[ERROR] ${line}`);
|
|
83
|
+
setOutputs((prev) => {
|
|
84
|
+
const newOutputs = [...prev];
|
|
85
|
+
newOutputs[idx] = [...(newOutputs[idx] || []), ...errorLines];
|
|
86
|
+
return newOutputs;
|
|
87
|
+
});
|
|
88
|
+
markAsRunning();
|
|
89
|
+
});
|
|
90
|
+
// Handle process exit
|
|
91
|
+
process.on("close", (code) => {
|
|
92
|
+
setStatuses((prev) => {
|
|
93
|
+
const newStatuses = [...prev];
|
|
94
|
+
newStatuses[idx] = code === 0 ? "stopped" : "error";
|
|
95
|
+
return newStatuses;
|
|
96
|
+
});
|
|
97
|
+
if (code !== 0 && code !== null) {
|
|
98
|
+
setOutputs((prev) => {
|
|
99
|
+
const newOutputs = [...prev];
|
|
100
|
+
newOutputs[idx] = [
|
|
101
|
+
...(newOutputs[idx] || []),
|
|
102
|
+
`\n[Process exited with code ${code}]`,
|
|
103
|
+
];
|
|
104
|
+
return newOutputs;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
process.on("error", (error) => {
|
|
109
|
+
setStatuses((prev) => {
|
|
110
|
+
const newStatuses = [...prev];
|
|
111
|
+
newStatuses[idx] = "error";
|
|
112
|
+
return newStatuses;
|
|
113
|
+
});
|
|
114
|
+
setOutputs((prev) => {
|
|
115
|
+
const newOutputs = [...prev];
|
|
116
|
+
newOutputs[idx] = [
|
|
117
|
+
...(newOutputs[idx] || []),
|
|
118
|
+
`\n[Process error: ${error.message}]`,
|
|
119
|
+
];
|
|
120
|
+
return newOutputs;
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
// Cleanup function
|
|
125
|
+
return () => {
|
|
126
|
+
processes.forEach(({ process }) => {
|
|
127
|
+
process.kill();
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
}, [processes, onPortDetected]);
|
|
131
|
+
const currentProcess = processes[activeTab];
|
|
132
|
+
if (!currentProcess) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsx(StatusLine, { activeTab: activeTab, tabs: processes.map((p) => p.name) }), _jsx(ProcessPane, { title: currentProcess.name, output: outputs[activeTab] || [], port: ports[activeTab], status: statuses[activeTab] || "starting" })] }));
|
|
136
|
+
}
|
package/dist/index.js
CHANGED
|
File without changes
|
package/dist/lib/mcp-storage.js
CHANGED
|
@@ -95,7 +95,11 @@ export function mcpConfigExists(name) {
|
|
|
95
95
|
*/
|
|
96
96
|
export function getMCPSummary(config) {
|
|
97
97
|
if (config.transport === "http") {
|
|
98
|
-
|
|
98
|
+
const parts = [`HTTP: ${config.url}`];
|
|
99
|
+
if (config.headers && Object.keys(config.headers).length > 0) {
|
|
100
|
+
parts.push(`(${Object.keys(config.headers).length} header${Object.keys(config.headers).length === 1 ? "" : "s"})`);
|
|
101
|
+
}
|
|
102
|
+
return parts.join(" ");
|
|
99
103
|
}
|
|
100
104
|
else {
|
|
101
105
|
const parts = [`Stdio: ${config.command}`];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if a port is available
|
|
3
|
+
*/
|
|
4
|
+
export declare function isPortAvailable(port: number): Promise<boolean>;
|
|
5
|
+
/**
|
|
6
|
+
* Find the next available port starting from the given port
|
|
7
|
+
*/
|
|
8
|
+
export declare function findAvailablePort(startPort: number, maxAttempts?: number): Promise<number>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createServer } from "node:net";
|
|
2
|
+
/**
|
|
3
|
+
* Check if a port is available
|
|
4
|
+
*/
|
|
5
|
+
export async function isPortAvailable(port) {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
const server = createServer();
|
|
8
|
+
server.once("error", (err) => {
|
|
9
|
+
if (err.code === "EADDRINUSE") {
|
|
10
|
+
resolve(false);
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
resolve(false);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
server.once("listening", () => {
|
|
17
|
+
server.close();
|
|
18
|
+
resolve(true);
|
|
19
|
+
});
|
|
20
|
+
server.listen(port);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Find the next available port starting from the given port
|
|
25
|
+
*/
|
|
26
|
+
export async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
27
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
28
|
+
const port = startPort + i;
|
|
29
|
+
const available = await isPortAvailable(port);
|
|
30
|
+
if (available) {
|
|
31
|
+
return port;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`Could not find an available port between ${startPort} and ${startPort + maxAttempts - 1}`);
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@townco/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"town": "./dist/index.js"
|
|
@@ -19,16 +19,16 @@
|
|
|
19
19
|
"build": "tsc"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@townco/tsconfig": "0.1.
|
|
22
|
+
"@townco/tsconfig": "0.1.7",
|
|
23
23
|
"@types/bun": "^1.3.1",
|
|
24
24
|
"@types/react": "^19.2.2"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@optique/core": "^0.6.2",
|
|
28
28
|
"@optique/run": "^0.6.2",
|
|
29
|
-
"@townco/agent": "0.1.
|
|
30
|
-
"@townco/secret": "0.1.
|
|
31
|
-
"@townco/ui": "0.1.
|
|
29
|
+
"@townco/agent": "0.1.15",
|
|
30
|
+
"@townco/secret": "0.1.10",
|
|
31
|
+
"@townco/ui": "0.1.10",
|
|
32
32
|
"@types/inquirer": "^9.0.9",
|
|
33
33
|
"ink": "^6.4.0",
|
|
34
34
|
"ink-text-input": "^6.0.0",
|