@townco/cli 0.1.3 → 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.
@@ -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
+ }
@@ -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
  },
@@ -117,6 +149,7 @@ export async function runCommand(options) {
117
149
  stdio: "inherit",
118
150
  env: {
119
151
  ...process.env,
152
+ ...configEnvVars,
120
153
  NODE_ENV: process.env.NODE_ENV || "production",
121
154
  },
122
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("create", object({
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",