@townco/cli 0.1.11 → 0.1.13

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/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # Town CLI
2
+
3
+ Run AI agents from the command line.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx @townco/cli configure # Set up API keys
9
+ npx @townco/cli create
10
+ npx @townco/cli run my-agent # Run in terminal (TUI)
11
+ npx @townco/cli run my-agent --gui # Run with web interface
12
+ ```
13
+
14
+ ## Commands
15
+
16
+ ### `create [name]` - Create an agent
17
+
18
+ ```bash
19
+ npx @townco/cli create # Interactive wizard
20
+ npx @townco/cli create --name my-agent # With name
21
+ npx @townco/cli create \
22
+ --name my-agent \
23
+ --model claude-sonnet-4-5-20250929 \
24
+ --tools web_search,todo_write \
25
+ --system-prompt "You are a helpful assistant"
26
+ ```
27
+
28
+ ### `list` - List all agents
29
+
30
+ ```bash
31
+ npx @townco/cli list
32
+ ```
33
+
34
+ ### `configure` - Set up API keys
35
+
36
+ ```bash
37
+ npx @townco/cli configure
38
+ ```
39
+
40
+ Sets `ANTHROPIC_API_KEY`, `EXA_API_KEY`, etc. in `~/.config/town/.env`
41
+
42
+ ### `run <name>` - Run an agent
43
+
44
+ ```bash
45
+ npx @townco/cli run my-agent # TUI mode (default)
46
+ npx @townco/cli run my-agent --gui # Web interface
47
+ npx @townco/cli run my-agent --http # HTTP API server
48
+ npx @townco/cli run my-agent --http --port 8080
49
+ ```
50
+
51
+ ### `delete <name>` - Delete an agent
52
+
53
+ ```bash
54
+ npx @townco/cli delete my-agent
55
+ ```
56
+
57
+ ## Models
58
+
59
+ - `claude-sonnet-4-5-20250929` - Latest (recommended)
60
+ - `claude-opus-4-20250514` - Most capable
61
+ - `claude-3-5-haiku-20241022` - Fastest
62
+
63
+ ## Tools
64
+
65
+ - `todo_write` - Task management
66
+ - `web_search` - Web search (requires EXA_API_KEY)
@@ -1,8 +1,9 @@
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(() => {
@@ -271,7 +205,7 @@ function CreateApp({ name: initialName, model: initialModel, tools: initialTools
271
205
  setStage("systemPrompt");
272
206
  // Attempt to open editor
273
207
  setIsEditingPrompt(true);
274
- const editorContent = await openEditor(agentDef.systemPrompt || "You are a helpful assistant.");
208
+ const editorContent = await openInEditor(agentDef.systemPrompt || "You are a helpful assistant.");
275
209
  if (editorContent !== null) {
276
210
  // Editor worked
277
211
  setAgentDef({ ...agentDef, systemPrompt: editorContent });
@@ -1,14 +1,9 @@
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({
8
- name: initialName,
9
- url: initialUrl,
10
- command: initialCommand,
11
- args: initialArgs,
12
- }: MCPAddProps): import("react/jsx-runtime").JSX.Element | null;
7
+ declare function MCPAddApp({ name: initialName, url: initialUrl, command: initialCommand, args: initialArgs, }: MCPAddProps): import("react/jsx-runtime").JSX.Element | null;
13
8
  export default MCPAddApp;
14
9
  export declare function runMCPAdd(props?: MCPAddProps): Promise<void>;
@@ -1,188 +1,61 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
2
  import { Box, render, Text, useApp, useInput } from "ink";
2
3
  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
-
6
5
  // ============================================================================
7
6
  // Main Component
8
7
  // ============================================================================
9
8
  function MCPListApp() {
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
- });
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" }) })] }));
174
46
  }
175
47
  // ============================================================================
176
48
  // Export and Runner
177
49
  // ============================================================================
178
50
  export default MCPListApp;
179
51
  export async function runMCPList() {
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
- }
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
+ }
188
61
  }
@@ -1,7 +1,7 @@
1
1
  export interface RunCommandOptions {
2
- name: string;
3
- http?: boolean;
4
- gui?: boolean;
5
- port?: number;
2
+ name: string;
3
+ http?: boolean;
4
+ gui?: boolean;
5
+ port?: number;
6
6
  }
7
7
  export declare function runCommand(options: RunCommandOptions): Promise<void>;
@@ -0,0 +1,6 @@
1
+ interface ToolAddProps {
2
+ name?: string;
3
+ }
4
+ declare function ToolAddApp({ name: initialName }: ToolAddProps): import("react/jsx-runtime").JSX.Element | null;
5
+ export default ToolAddApp;
6
+ export declare function runToolAdd(props?: ToolAddProps): Promise<void>;