@townco/cli 0.1.2 → 0.1.4
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/configure.d.ts +1 -0
- package/dist/commands/configure.js +146 -0
- package/dist/commands/run.js +41 -2
- package/dist/index.js +7 -1
- package/package.json +2 -1
- package/tui/App.d.ts +5 -0
- package/tui/App.js +28 -0
- package/tui/cli.d.ts +12 -0
- package/tui/cli.js +14 -0
- package/tui/index.d.ts +2 -0
- package/tui/index.js +30 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function configureCommand(): Promise<void>;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import inquirer from "inquirer";
|
|
6
|
+
const ENV_KEYS = [
|
|
7
|
+
{
|
|
8
|
+
key: "ANTHROPIC_API_KEY",
|
|
9
|
+
description: "Anthropic API key for Claude models",
|
|
10
|
+
required: true,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
key: "EXA_API_KEY",
|
|
14
|
+
description: "Exa API key for web search tool",
|
|
15
|
+
required: false,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
key: "OPENAI_API_KEY",
|
|
19
|
+
description: "OpenAI API key (optional, for OpenAI models)",
|
|
20
|
+
required: false,
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
function getConfigDir() {
|
|
24
|
+
return join(homedir(), ".config", "town");
|
|
25
|
+
}
|
|
26
|
+
function getEnvFilePath() {
|
|
27
|
+
return join(getConfigDir(), ".env");
|
|
28
|
+
}
|
|
29
|
+
async function loadExistingEnv() {
|
|
30
|
+
const envPath = getEnvFilePath();
|
|
31
|
+
if (!existsSync(envPath)) {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
const content = await readFile(envPath, "utf-8");
|
|
35
|
+
const config = {};
|
|
36
|
+
for (const line of content.split("\n")) {
|
|
37
|
+
const trimmed = line.trim();
|
|
38
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
39
|
+
continue;
|
|
40
|
+
const [key, ...valueParts] = trimmed.split("=");
|
|
41
|
+
const value = valueParts.join("=").trim();
|
|
42
|
+
if (key && value) {
|
|
43
|
+
config[key.trim()] = value;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return config;
|
|
47
|
+
}
|
|
48
|
+
async function saveEnv(config) {
|
|
49
|
+
const configDir = getConfigDir();
|
|
50
|
+
await mkdir(configDir, { recursive: true });
|
|
51
|
+
const lines = [
|
|
52
|
+
"# Town CLI Configuration",
|
|
53
|
+
"# Environment variables for Town agents",
|
|
54
|
+
"",
|
|
55
|
+
];
|
|
56
|
+
for (const { key, description } of ENV_KEYS) {
|
|
57
|
+
const value = config[key];
|
|
58
|
+
if (value) {
|
|
59
|
+
lines.push(`# ${description}`);
|
|
60
|
+
lines.push(`${key}=${value}`);
|
|
61
|
+
lines.push("");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
await writeFile(getEnvFilePath(), lines.join("\n"), "utf-8");
|
|
65
|
+
}
|
|
66
|
+
export async function configureCommand() {
|
|
67
|
+
console.log("🔧 Town Configuration\n");
|
|
68
|
+
const existingConfig = await loadExistingEnv();
|
|
69
|
+
const hasExisting = Object.keys(existingConfig).length > 0;
|
|
70
|
+
if (hasExisting) {
|
|
71
|
+
console.log("Found existing configuration:\n");
|
|
72
|
+
for (const { key, description } of ENV_KEYS) {
|
|
73
|
+
const value = existingConfig[key];
|
|
74
|
+
if (value) {
|
|
75
|
+
const masked = value.slice(0, 4) + "..." + value.slice(-4);
|
|
76
|
+
console.log(` ${key}: ${masked}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
console.log("");
|
|
80
|
+
const { action } = await inquirer.prompt([
|
|
81
|
+
{
|
|
82
|
+
type: "list",
|
|
83
|
+
name: "action",
|
|
84
|
+
message: "What would you like to do?",
|
|
85
|
+
choices: [
|
|
86
|
+
{ name: "Update existing keys", value: "update" },
|
|
87
|
+
{ name: "Add new keys", value: "add" },
|
|
88
|
+
{ name: "Reconfigure all keys", value: "reconfigure" },
|
|
89
|
+
{ name: "Cancel", value: "cancel" },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
]);
|
|
93
|
+
if (action === "cancel") {
|
|
94
|
+
console.log("Configuration cancelled.");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (action === "reconfigure") {
|
|
98
|
+
// Clear existing config for fresh start
|
|
99
|
+
Object.keys(existingConfig).forEach((key) => {
|
|
100
|
+
delete existingConfig[key];
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const newConfig = { ...existingConfig };
|
|
105
|
+
// Prompt for each key
|
|
106
|
+
for (const { key, description, required } of ENV_KEYS) {
|
|
107
|
+
const hasValue = !!existingConfig[key];
|
|
108
|
+
if (hasValue && !hasExisting) {
|
|
109
|
+
// Skip if we're only adding new keys and this one exists
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const { shouldSet } = await inquirer.prompt([
|
|
113
|
+
{
|
|
114
|
+
type: "confirm",
|
|
115
|
+
name: "shouldSet",
|
|
116
|
+
message: hasValue
|
|
117
|
+
? `Update ${key}? (${description})`
|
|
118
|
+
: `Set ${key}? (${description})`,
|
|
119
|
+
default: required || !hasValue,
|
|
120
|
+
},
|
|
121
|
+
]);
|
|
122
|
+
if (shouldSet) {
|
|
123
|
+
const { value } = await inquirer.prompt([
|
|
124
|
+
{
|
|
125
|
+
type: "password",
|
|
126
|
+
name: "value",
|
|
127
|
+
message: `Enter ${key}:`,
|
|
128
|
+
mask: "*",
|
|
129
|
+
validate: (input) => {
|
|
130
|
+
if (required && !input.trim()) {
|
|
131
|
+
return "This key is required";
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
]);
|
|
137
|
+
if (value.trim()) {
|
|
138
|
+
newConfig[key] = value.trim();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Save configuration
|
|
143
|
+
await saveEnv(newConfig);
|
|
144
|
+
console.log(`\n✅ Configuration saved to ${getEnvFilePath()}`);
|
|
145
|
+
console.log("\nThese environment variables will be automatically loaded when running agents.");
|
|
146
|
+
}
|
package/dist/commands/run.js
CHANGED
|
@@ -1,8 +1,37 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
2
5
|
import { join } from "node:path";
|
|
3
6
|
import { agentExists, getAgentPath } from "@townco/agent/storage";
|
|
7
|
+
async function loadEnvVars() {
|
|
8
|
+
const envPath = join(homedir(), ".config", "town", ".env");
|
|
9
|
+
const envVars = {};
|
|
10
|
+
if (!existsSync(envPath)) {
|
|
11
|
+
return envVars;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const content = await readFile(envPath, "utf-8");
|
|
15
|
+
for (const line of content.split("\n")) {
|
|
16
|
+
const trimmed = line.trim();
|
|
17
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
18
|
+
continue;
|
|
19
|
+
const [key, ...valueParts] = trimmed.split("=");
|
|
20
|
+
const value = valueParts.join("=").trim();
|
|
21
|
+
if (key && value) {
|
|
22
|
+
envVars[key.trim()] = value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
console.warn(`Warning: Could not load environment variables from ${envPath}`);
|
|
28
|
+
}
|
|
29
|
+
return envVars;
|
|
30
|
+
}
|
|
4
31
|
export async function runCommand(options) {
|
|
5
32
|
const { name, http = false, gui = false, port = 3100 } = options;
|
|
33
|
+
// Load environment variables from ~/.config/town/.env
|
|
34
|
+
const configEnvVars = await loadEnvVars();
|
|
6
35
|
// Check if agent exists
|
|
7
36
|
const exists = await agentExists(name);
|
|
8
37
|
if (!exists) {
|
|
@@ -35,6 +64,7 @@ export async function runCommand(options) {
|
|
|
35
64
|
stdio: "pipe",
|
|
36
65
|
env: {
|
|
37
66
|
...process.env,
|
|
67
|
+
...configEnvVars,
|
|
38
68
|
NODE_ENV: process.env.NODE_ENV || "production",
|
|
39
69
|
PORT: port.toString(),
|
|
40
70
|
},
|
|
@@ -51,6 +81,7 @@ export async function runCommand(options) {
|
|
|
51
81
|
stdio: "inherit",
|
|
52
82
|
env: {
|
|
53
83
|
...process.env,
|
|
84
|
+
...configEnvVars,
|
|
54
85
|
VITE_AGENT_URL: `http://localhost:${port}`,
|
|
55
86
|
},
|
|
56
87
|
});
|
|
@@ -87,6 +118,7 @@ export async function runCommand(options) {
|
|
|
87
118
|
stdio: "inherit",
|
|
88
119
|
env: {
|
|
89
120
|
...process.env,
|
|
121
|
+
...configEnvVars,
|
|
90
122
|
NODE_ENV: process.env.NODE_ENV || "production",
|
|
91
123
|
PORT: port.toString(),
|
|
92
124
|
},
|
|
@@ -105,12 +137,19 @@ export async function runCommand(options) {
|
|
|
105
137
|
}
|
|
106
138
|
// Default: Start TUI interface with the agent
|
|
107
139
|
console.log(`Starting interactive terminal for agent "${name}"...\n`);
|
|
108
|
-
//
|
|
109
|
-
const
|
|
140
|
+
// Get path to TUI app from CLI package
|
|
141
|
+
const { fileURLToPath } = await import("node:url");
|
|
142
|
+
const { dirname } = await import("node:path");
|
|
143
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
144
|
+
const cliRoot = join(dirname(currentFile), "..", "..");
|
|
145
|
+
const tuiPath = join(cliRoot, "tui", "index.js");
|
|
146
|
+
// Run TUI with the agent
|
|
147
|
+
const tuiProcess = spawn("bun", [tuiPath, "--agent", binPath], {
|
|
110
148
|
cwd: agentPath,
|
|
111
149
|
stdio: "inherit",
|
|
112
150
|
env: {
|
|
113
151
|
...process.env,
|
|
152
|
+
...configEnvVars,
|
|
114
153
|
NODE_ENV: process.env.NODE_ENV || "production",
|
|
115
154
|
},
|
|
116
155
|
});
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { run } from "@optique/run";
|
|
|
6
6
|
import { createSecret, deleteSecret, genenv, listSecrets } from "@townco/secret";
|
|
7
7
|
import inquirer from "inquirer";
|
|
8
8
|
import { match } from "ts-pattern";
|
|
9
|
+
import { configureCommand } from "./commands/configure.js";
|
|
9
10
|
import { createCommand } from "./commands/create.js";
|
|
10
11
|
import { deleteCommand } from "./commands/delete.js";
|
|
11
12
|
import { editCommand } from "./commands/edit.js";
|
|
@@ -25,7 +26,9 @@ async function promptSecret(secretName) {
|
|
|
25
26
|
]);
|
|
26
27
|
return answers.value;
|
|
27
28
|
}
|
|
28
|
-
const parser = or(command("deploy", constant("deploy"), { brief: message `Deploy a Town.` }), command("
|
|
29
|
+
const parser = or(command("deploy", constant("deploy"), { brief: message `Deploy a Town.` }), command("configure", constant("configure"), {
|
|
30
|
+
brief: message `Configure environment variables.`,
|
|
31
|
+
}), command("create", object({
|
|
29
32
|
command: constant("create"),
|
|
30
33
|
name: optional(option("-n", "--name", string())),
|
|
31
34
|
model: optional(option("-m", "--model", string())),
|
|
@@ -69,6 +72,9 @@ async function main(parser, meta) {
|
|
|
69
72
|
await match(result)
|
|
70
73
|
// TODO
|
|
71
74
|
.with("deploy", async () => { })
|
|
75
|
+
.with("configure", async () => {
|
|
76
|
+
await configureCommand();
|
|
77
|
+
})
|
|
72
78
|
.with({ command: "create" }, async ({ name, model, tools, systemPrompt }) => {
|
|
73
79
|
// Create command starts a long-running Ink session
|
|
74
80
|
// Only pass defined properties to satisfy exactOptionalPropertyTypes
|
package/package.json
CHANGED
package/tui/App.d.ts
ADDED
package/tui/App.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AcpClient } from "@townco/ui";
|
|
3
|
+
import { ChatView } from "@townco/ui/tui";
|
|
4
|
+
import { Box, Text } from "ink";
|
|
5
|
+
import { useMemo } from "react";
|
|
6
|
+
export function App({ config }) {
|
|
7
|
+
// Create ACP client
|
|
8
|
+
const client = useMemo(() => {
|
|
9
|
+
try {
|
|
10
|
+
const newClient = new AcpClient({
|
|
11
|
+
type: "stdio",
|
|
12
|
+
options: {
|
|
13
|
+
agentPath: config.agentPath,
|
|
14
|
+
workingDirectory: config.workingDir ?? process.cwd(),
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
return newClient;
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
console.error("Failed to create client:", error);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}, [config.agentPath, config.workingDir]);
|
|
24
|
+
if (!client) {
|
|
25
|
+
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Text, { color: "red", bold: true, children: "Error: Failed to create ACP client" }), _jsx(Text, { color: "gray", children: "Please check your agent path and try again." })] }));
|
|
26
|
+
}
|
|
27
|
+
return _jsx(ChatView, { client: client });
|
|
28
|
+
}
|
package/tui/cli.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { InferValue } from "@optique/core/parser";
|
|
2
|
+
/**
|
|
3
|
+
* CLI configuration using Optique
|
|
4
|
+
*/
|
|
5
|
+
export declare const cliParser: import("@optique/core/parser").Parser<{
|
|
6
|
+
readonly agentPath: string;
|
|
7
|
+
readonly workingDir: string | undefined;
|
|
8
|
+
}, {
|
|
9
|
+
readonly agentPath: import("@optique/core/valueparser").ValueParserResult<string> | undefined;
|
|
10
|
+
readonly workingDir: [[import("@optique/core/valueparser").ValueParserResult<string> | undefined] | undefined] | undefined;
|
|
11
|
+
}>;
|
|
12
|
+
export type CliConfig = InferValue<typeof cliParser>;
|
package/tui/cli.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { text } from "@optique/core/message";
|
|
2
|
+
import { object, option, optional, withDefault } from "@optique/core/parser";
|
|
3
|
+
import { string } from "@optique/core/valueparser";
|
|
4
|
+
/**
|
|
5
|
+
* CLI configuration using Optique
|
|
6
|
+
*/
|
|
7
|
+
export const cliParser = object({
|
|
8
|
+
agentPath: option("-a", "--agent", string(), {
|
|
9
|
+
description: [text("Path to the ACP agent executable")],
|
|
10
|
+
}),
|
|
11
|
+
workingDir: withDefault(optional(option("-d", "--dir", string(), {
|
|
12
|
+
description: [text("Working directory for the agent")],
|
|
13
|
+
})), process.cwd()),
|
|
14
|
+
});
|
package/tui/index.d.ts
ADDED
package/tui/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { run } from "@optique/run";
|
|
4
|
+
import { render } from "ink";
|
|
5
|
+
import { App } from "./App.js";
|
|
6
|
+
import { cliParser } from "./cli.js";
|
|
7
|
+
/**
|
|
8
|
+
* Agent Hub TUI
|
|
9
|
+
* Terminal chat interface for ACP agents
|
|
10
|
+
*/
|
|
11
|
+
async function main() {
|
|
12
|
+
// Parse CLI arguments
|
|
13
|
+
const config = run(cliParser, {
|
|
14
|
+
help: "both", // Show help for both commands and the program
|
|
15
|
+
programName: "agent-tui",
|
|
16
|
+
description: [
|
|
17
|
+
{
|
|
18
|
+
type: "text",
|
|
19
|
+
text: "Terminal chat interface for Agent Client Protocol (ACP) agents",
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
version: "0.0.1",
|
|
23
|
+
});
|
|
24
|
+
// Render the app
|
|
25
|
+
render(_jsx(App, { config: config }));
|
|
26
|
+
}
|
|
27
|
+
main().catch((error) => {
|
|
28
|
+
console.error("Fatal error:", error);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|