@townco/cli 0.1.21 ā 0.1.23
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.js +22 -14
- package/dist/commands/create-project.d.ts +7 -0
- package/dist/commands/create-project.js +22 -0
- package/dist/commands/create.d.ts +7 -5
- package/dist/commands/create.js +11 -8
- package/dist/commands/edit.js +17 -7
- package/dist/commands/mcp-add.d.ts +5 -10
- package/dist/commands/run.js +24 -11
- package/dist/commands/tool-stub.d.ts +6 -0
- package/dist/commands/tool-stub.js +376 -0
- package/dist/index.js +58 -10
- package/package.json +5 -5
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
3
|
import { join } from "node:path";
|
|
4
|
+
import { isInsideTownProject } from "@townco/agent/storage";
|
|
5
5
|
import inquirer from "inquirer";
|
|
6
6
|
const ENV_KEYS = [
|
|
7
7
|
{
|
|
@@ -20,14 +20,20 @@ const ENV_KEYS = [
|
|
|
20
20
|
required: false,
|
|
21
21
|
},
|
|
22
22
|
];
|
|
23
|
-
function
|
|
24
|
-
|
|
23
|
+
async function getProjectRoot() {
|
|
24
|
+
const projectRoot = await isInsideTownProject();
|
|
25
|
+
if (projectRoot === null) {
|
|
26
|
+
console.error("ā Error: Not inside a Town project.");
|
|
27
|
+
console.error("\nRun `town create --init <path>` to initialize a project first.");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
return projectRoot;
|
|
25
31
|
}
|
|
26
|
-
function getEnvFilePath() {
|
|
27
|
-
return join(
|
|
32
|
+
function getEnvFilePath(projectRoot) {
|
|
33
|
+
return join(projectRoot, ".env");
|
|
28
34
|
}
|
|
29
|
-
async function loadExistingEnv() {
|
|
30
|
-
const envPath = getEnvFilePath();
|
|
35
|
+
async function loadExistingEnv(projectRoot) {
|
|
36
|
+
const envPath = getEnvFilePath(projectRoot);
|
|
31
37
|
if (!existsSync(envPath)) {
|
|
32
38
|
return {};
|
|
33
39
|
}
|
|
@@ -45,9 +51,9 @@ async function loadExistingEnv() {
|
|
|
45
51
|
}
|
|
46
52
|
return config;
|
|
47
53
|
}
|
|
48
|
-
async function saveEnv(config) {
|
|
49
|
-
|
|
50
|
-
await mkdir(
|
|
54
|
+
async function saveEnv(config, projectRoot) {
|
|
55
|
+
// Ensure project root exists (it should, but just to be safe)
|
|
56
|
+
await mkdir(projectRoot, { recursive: true });
|
|
51
57
|
const lines = [
|
|
52
58
|
"# Town CLI Configuration",
|
|
53
59
|
"# Environment variables for Town agents",
|
|
@@ -61,11 +67,13 @@ async function saveEnv(config) {
|
|
|
61
67
|
lines.push("");
|
|
62
68
|
}
|
|
63
69
|
}
|
|
64
|
-
await writeFile(getEnvFilePath(), lines.join("\n"), "utf-8");
|
|
70
|
+
await writeFile(getEnvFilePath(projectRoot), lines.join("\n"), "utf-8");
|
|
65
71
|
}
|
|
66
72
|
export async function configureCommand() {
|
|
67
73
|
console.log("š§ Town Configuration\n");
|
|
68
|
-
|
|
74
|
+
// Get project root (will error and exit if not in a project)
|
|
75
|
+
const projectRoot = await getProjectRoot();
|
|
76
|
+
const existingConfig = await loadExistingEnv(projectRoot);
|
|
69
77
|
const hasExisting = Object.keys(existingConfig).length > 0;
|
|
70
78
|
if (hasExisting) {
|
|
71
79
|
console.log("Found existing configuration:\n");
|
|
@@ -140,7 +148,7 @@ export async function configureCommand() {
|
|
|
140
148
|
}
|
|
141
149
|
}
|
|
142
150
|
// Save configuration
|
|
143
|
-
await saveEnv(newConfig);
|
|
144
|
-
console.log(`\nā
Configuration saved to ${getEnvFilePath()}`);
|
|
151
|
+
await saveEnv(newConfig, projectRoot);
|
|
152
|
+
console.log(`\nā
Configuration saved to ${getEnvFilePath(projectRoot)}`);
|
|
145
153
|
console.log("\nThese environment variables will be automatically loaded when running agents.");
|
|
146
154
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { scaffoldProject } from "@townco/agent/scaffold";
|
|
3
|
+
/**
|
|
4
|
+
* Create a new standalone project with agents and tools
|
|
5
|
+
*/
|
|
6
|
+
export async function createProjectCommand(props = {}) {
|
|
7
|
+
const { path = process.cwd() } = props;
|
|
8
|
+
// Resolve the path to absolute
|
|
9
|
+
const projectPath = resolve(path);
|
|
10
|
+
console.log(`Creating new Town agents project at: ${projectPath}`);
|
|
11
|
+
const result = await scaffoldProject({
|
|
12
|
+
path: projectPath,
|
|
13
|
+
});
|
|
14
|
+
if (result.success) {
|
|
15
|
+
console.log("\nā Project created successfully!");
|
|
16
|
+
console.log(`\nProject path: ${result.path}`);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
console.error(`\nā Error creating project: ${result.error}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -1,8 +1,10 @@
|
|
|
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
|
+
agentsDir: string;
|
|
7
8
|
}
|
|
8
9
|
export declare function createCommand(props: CreateCommandProps): Promise<void>;
|
|
10
|
+
export {};
|
package/dist/commands/create.js
CHANGED
|
@@ -52,7 +52,7 @@ function NameInput({ nameInput, setNameInput, onSubmit }) {
|
|
|
52
52
|
});
|
|
53
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" }) })] }));
|
|
54
54
|
}
|
|
55
|
-
function CreateApp({ name: initialName, model: initialModel, tools: initialTools, systemPrompt: initialSystemPrompt, overwrite = false, }) {
|
|
55
|
+
function CreateApp({ name: initialName, model: initialModel, tools: initialTools, systemPrompt: initialSystemPrompt, overwrite = false, agentsDir, }) {
|
|
56
56
|
// Determine the starting stage based on what's provided
|
|
57
57
|
const determineInitialStage = () => {
|
|
58
58
|
if (!initialName)
|
|
@@ -119,14 +119,17 @@ function CreateApp({ name: initialName, model: initialModel, tools: initialTools
|
|
|
119
119
|
}
|
|
120
120
|
const name = agentDef.name;
|
|
121
121
|
const model = agentDef.model;
|
|
122
|
+
const definition = {
|
|
123
|
+
model,
|
|
124
|
+
systemPrompt: agentDef.systemPrompt || null,
|
|
125
|
+
tools: agentDef.tools || [],
|
|
126
|
+
};
|
|
127
|
+
// Create agent in project
|
|
122
128
|
scaffoldAgent({
|
|
123
129
|
name,
|
|
124
|
-
definition
|
|
125
|
-
model,
|
|
126
|
-
systemPrompt: agentDef.systemPrompt || null,
|
|
127
|
-
tools: agentDef.tools || [],
|
|
128
|
-
},
|
|
130
|
+
definition,
|
|
129
131
|
overwrite,
|
|
132
|
+
agentsDir,
|
|
130
133
|
}).then((result) => {
|
|
131
134
|
if (result.success) {
|
|
132
135
|
setScaffoldStatus("done");
|
|
@@ -142,7 +145,7 @@ function CreateApp({ name: initialName, model: initialModel, tools: initialTools
|
|
|
142
145
|
}
|
|
143
146
|
});
|
|
144
147
|
}
|
|
145
|
-
}, [stage, scaffoldStatus, agentDef, overwrite]);
|
|
148
|
+
}, [stage, scaffoldStatus, agentDef, overwrite, agentsDir]);
|
|
146
149
|
// Name stage
|
|
147
150
|
if (stage === "name") {
|
|
148
151
|
return (_jsx(NameInput, { nameInput: nameInput, setNameInput: setNameInput, onSubmit: () => {
|
|
@@ -294,7 +297,7 @@ function CreateApp({ name: initialName, model: initialModel, tools: initialTools
|
|
|
294
297
|
}
|
|
295
298
|
if (scaffoldStatus === "done") {
|
|
296
299
|
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
|
|
300
|
+
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 agent: bun agents/", agentDef.name, "/index.ts stdio"] }), _jsxs(Text, { dimColor: true, children: ["GUI: cd agents/", agentDef.name, "/gui && bun run dev"] }), _jsxs(Text, { dimColor: true, children: ["TUI: cd agents/", agentDef.name, "/tui && bun start"] })] })] }));
|
|
298
301
|
}
|
|
299
302
|
}
|
|
300
303
|
return null;
|
package/dist/commands/edit.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { isInsideTownProject } from "@townco/agent/storage";
|
|
4
4
|
import { createCommand } from "./create.js";
|
|
5
|
-
async function loadAgentConfig(name) {
|
|
5
|
+
async function loadAgentConfig(name, agentPath) {
|
|
6
6
|
try {
|
|
7
|
-
const agentPath = getAgentPath(name);
|
|
8
7
|
const indexPath = join(agentPath, "index.ts");
|
|
9
8
|
const content = await readFile(indexPath, "utf-8");
|
|
10
9
|
// Parse model
|
|
@@ -55,15 +54,25 @@ async function loadAgentConfig(name) {
|
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
56
|
export async function editCommand(name) {
|
|
58
|
-
// Check if
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
57
|
+
// Check if we're inside a Town project
|
|
58
|
+
const projectRoot = await isInsideTownProject();
|
|
59
|
+
if (projectRoot === null) {
|
|
60
|
+
console.error("Error: Not inside a Town project.\n\n" +
|
|
61
|
+
"Please run 'town edit' inside a project directory.");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
const agentPath = join(projectRoot, "agents", name);
|
|
65
|
+
// Check if the agent exists
|
|
66
|
+
try {
|
|
67
|
+
await readFile(join(agentPath, "agent.json"), "utf-8");
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
61
70
|
console.error(`Error: Agent "${name}" not found.`);
|
|
62
71
|
console.log('\nCreate an agent with "town create" or list agents with "town list"');
|
|
63
72
|
process.exit(1);
|
|
64
73
|
}
|
|
65
74
|
// Load existing config
|
|
66
|
-
const config = await loadAgentConfig(name);
|
|
75
|
+
const config = await loadAgentConfig(name, agentPath);
|
|
67
76
|
if (!config) {
|
|
68
77
|
console.error(`Error: Failed to load agent configuration for "${name}".`);
|
|
69
78
|
process.exit(1);
|
|
@@ -78,5 +87,6 @@ export async function editCommand(name) {
|
|
|
78
87
|
systemPrompt: config.systemPrompt,
|
|
79
88
|
}),
|
|
80
89
|
overwrite: true,
|
|
90
|
+
agentsDir: join(projectRoot, "agents"),
|
|
81
91
|
});
|
|
82
92
|
}
|
|
@@ -1,14 +1,9 @@
|
|
|
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({
|
|
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>;
|
package/dist/commands/run.js
CHANGED
|
@@ -2,9 +2,8 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
5
|
import { join } from "node:path";
|
|
7
|
-
import {
|
|
6
|
+
import { isInsideTownProject } from "@townco/agent/storage";
|
|
8
7
|
import { createLogger } from "@townco/agent/utils";
|
|
9
8
|
import { AcpClient } from "@townco/ui";
|
|
10
9
|
import { ChatView } from "@townco/ui/tui";
|
|
@@ -100,10 +99,11 @@ function GuiRunner({ agentProcess, guiProcess, agentPort, agentPath, logger, onE
|
|
|
100
99
|
], [agentProcess, guiProcess, agentPort]);
|
|
101
100
|
return (_jsx(TabbedOutput, { processes: processes, logsDir: join(agentPath, ".logs"), onExit: onExit, onPortDetected: handlePortDetected }));
|
|
102
101
|
}
|
|
103
|
-
async function loadEnvVars(logger) {
|
|
104
|
-
const envPath = join(
|
|
102
|
+
async function loadEnvVars(projectRoot, logger) {
|
|
103
|
+
const envPath = join(projectRoot, ".env");
|
|
105
104
|
const envVars = {};
|
|
106
105
|
if (!existsSync(envPath)) {
|
|
106
|
+
logger?.debug("No .env file found in project root", { path: envPath });
|
|
107
107
|
return envVars;
|
|
108
108
|
}
|
|
109
109
|
try {
|
|
@@ -133,16 +133,29 @@ async function loadEnvVars(logger) {
|
|
|
133
133
|
}
|
|
134
134
|
export async function runCommand(options) {
|
|
135
135
|
const { name, http = false, gui = false, port = 3100 } = options;
|
|
136
|
-
//
|
|
137
|
-
const
|
|
136
|
+
// Check if we're inside a Town project
|
|
137
|
+
const projectRoot = await isInsideTownProject();
|
|
138
|
+
if (projectRoot === null) {
|
|
139
|
+
console.error("Error: Not inside a Town project.");
|
|
140
|
+
console.log('\nPlease run "town run" inside a project directory, or run:\n' +
|
|
141
|
+
" town create --init <path>\n" +
|
|
142
|
+
"to create a project.");
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
// Load environment variables from project .env
|
|
146
|
+
const configEnvVars = await loadEnvVars(projectRoot);
|
|
147
|
+
// Resolve agent path within the project
|
|
148
|
+
const agentPath = join(projectRoot, "agents", name);
|
|
138
149
|
// Check if agent exists
|
|
139
|
-
|
|
140
|
-
|
|
150
|
+
try {
|
|
151
|
+
const { stat } = await import("node:fs/promises");
|
|
152
|
+
await stat(agentPath);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
141
155
|
console.error(`Error: Agent "${name}" not found.`);
|
|
142
156
|
console.log('\nCreate an agent with "town create" or list agents with "town list"');
|
|
143
157
|
process.exit(1);
|
|
144
158
|
}
|
|
145
|
-
const agentPath = getAgentPath(name);
|
|
146
159
|
const binPath = join(agentPath, "bin.ts");
|
|
147
160
|
// Create logger with agent directory as logs location
|
|
148
161
|
const logger = createLogger("cli", "debug", {
|
|
@@ -200,8 +213,8 @@ export async function runCommand(options) {
|
|
|
200
213
|
PORT: availablePort.toString(),
|
|
201
214
|
},
|
|
202
215
|
});
|
|
203
|
-
// Start the GUI dev server
|
|
204
|
-
const guiProcess = spawn("
|
|
216
|
+
// Start the GUI dev server (no package.json, run vite directly)
|
|
217
|
+
const guiProcess = spawn("bunx", ["vite"], {
|
|
205
218
|
cwd: guiPath,
|
|
206
219
|
stdio: ["ignore", "pipe", "pipe"], // Pipe stdout/stderr for capture
|
|
207
220
|
env: {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
interface ToolStubProps {
|
|
2
|
+
name?: string;
|
|
3
|
+
}
|
|
4
|
+
declare function ToolStubApp({ name: initialName }: ToolStubProps): import("react/jsx-runtime").JSX.Element | null;
|
|
5
|
+
export default ToolStubApp;
|
|
6
|
+
export declare function runToolStub(props?: ToolStubProps): Promise<void>;
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { copyFileSync, existsSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { getAgentPath, listAgents } from "@townco/agent/storage";
|
|
7
|
+
import { MultiSelect } from "@townco/ui/tui";
|
|
8
|
+
import { Box, render, Text, useApp, useInput } from "ink";
|
|
9
|
+
import TextInput from "ink-text-input";
|
|
10
|
+
import { useEffect, useState } from "react";
|
|
11
|
+
import { ensureToolsDir, getToolsDir, saveToolConfig, toolConfigExists, } from "../lib/tool-storage";
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Helper Functions
|
|
14
|
+
// ============================================================================
|
|
15
|
+
/**
|
|
16
|
+
* Normalize a tool name to kebab-case for use as a filename
|
|
17
|
+
*/
|
|
18
|
+
function normalizeToolName(name) {
|
|
19
|
+
return name
|
|
20
|
+
.trim()
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
23
|
+
.replace(/^-+|-+$/g, "");
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Convert kebab-case or snake_case to camelCase for function names
|
|
27
|
+
*/
|
|
28
|
+
function toCamelCase(str) {
|
|
29
|
+
return str.replace(/[-_](.)/g, (_, char) => char.toUpperCase());
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Convert a name to snake_case for the tool name export
|
|
33
|
+
*/
|
|
34
|
+
function toSnakeCase(str) {
|
|
35
|
+
return str
|
|
36
|
+
.trim()
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
39
|
+
.replace(/^_+|_+$/g, "");
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Generate the stub template content
|
|
43
|
+
*/
|
|
44
|
+
function generateStubTemplate(toolName) {
|
|
45
|
+
const snakeCaseName = toolName ? toSnakeCase(toolName) : "tool_name";
|
|
46
|
+
const camelCaseName = toolName ? toCamelCase(toolName) : "toolName";
|
|
47
|
+
const description = toolName
|
|
48
|
+
? `Describe what ${toolName} does here`
|
|
49
|
+
: "Describe what your tool does here";
|
|
50
|
+
return `// biome-ignore lint/suspicious/noExplicitAny: .
|
|
51
|
+
export const schema = (z: any) =>
|
|
52
|
+
z.object({
|
|
53
|
+
// Define your input parameters here
|
|
54
|
+
// Example: message: z.string().describe("The message to process"),
|
|
55
|
+
input: z.string().optional(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export const name = "${snakeCaseName}";
|
|
59
|
+
export const description = "${description}";
|
|
60
|
+
|
|
61
|
+
export default function ${camelCaseName}(_input: unknown) {
|
|
62
|
+
// TODO: Implement your tool logic here
|
|
63
|
+
|
|
64
|
+
return "result";
|
|
65
|
+
}
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Open a file in the user's editor and wait for it to close
|
|
70
|
+
*/
|
|
71
|
+
async function openInEditor(filePath) {
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
// Get editor from environment, fallback to vi
|
|
74
|
+
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
75
|
+
// Parse editor command (might include args like "code --wait")
|
|
76
|
+
const editorParts = editor.split(" ");
|
|
77
|
+
const editorCommand = editorParts[0] || "vi";
|
|
78
|
+
const editorArgs = [...editorParts.slice(1), filePath];
|
|
79
|
+
const child = spawn(editorCommand, editorArgs, {
|
|
80
|
+
stdio: "inherit",
|
|
81
|
+
});
|
|
82
|
+
child.on("exit", (code) => {
|
|
83
|
+
if (code === 0 || code === null) {
|
|
84
|
+
resolve();
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
reject(new Error(`Editor exited with code ${code}`));
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
child.on("error", (error) => {
|
|
91
|
+
reject(error);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function TextInputStage({ title, value, onChange, onSubmit, onCancel, placeholder, }) {
|
|
96
|
+
useInput((_input, key) => {
|
|
97
|
+
if (key.escape) {
|
|
98
|
+
onCancel();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: title }) }), _jsxs(Box, { children: [_jsxs(Text, { children: [">", " "] }), _jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit, ...(placeholder && { placeholder }) })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: Continue \u2022 Esc: Back" }) })] }));
|
|
102
|
+
}
|
|
103
|
+
function NameInputStage({ value, onChange, onNext, onBack, }) {
|
|
104
|
+
const [localError, setLocalError] = useState(null);
|
|
105
|
+
const handleSubmit = () => {
|
|
106
|
+
const trimmed = value.trim();
|
|
107
|
+
if (!trimmed) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Check for duplicate
|
|
111
|
+
if (toolConfigExists(trimmed)) {
|
|
112
|
+
setLocalError(`Tool "${trimmed}" already exists`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
onNext(trimmed);
|
|
116
|
+
};
|
|
117
|
+
const handleChange = (newValue) => {
|
|
118
|
+
if (localError) {
|
|
119
|
+
setLocalError(null);
|
|
120
|
+
}
|
|
121
|
+
onChange(newValue);
|
|
122
|
+
};
|
|
123
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(TextInputStage, { title: "Enter tool name:", value: value, onChange: handleChange, onSubmit: handleSubmit, onCancel: onBack, placeholder: "my-custom-tool" }), localError && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["\u274C ", localError] }) }))] }));
|
|
124
|
+
}
|
|
125
|
+
function NoAgentsMessage({ onNext }) {
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
const timer = setTimeout(() => {
|
|
128
|
+
onNext();
|
|
129
|
+
}, 1000);
|
|
130
|
+
return () => clearTimeout(timer);
|
|
131
|
+
}, [onNext]);
|
|
132
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "No agents found. Tool will be saved globally." }), _jsx(Text, { dimColor: true, children: "You can attach it to agents later." })] }));
|
|
133
|
+
}
|
|
134
|
+
function AgentSelectionStage({ selectedAgents, onSelectedAgentsChange, onNext, onBack, }) {
|
|
135
|
+
const [availableAgents, setAvailableAgents] = useState([]);
|
|
136
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
// Fetch available agents
|
|
139
|
+
const fetchAgents = async () => {
|
|
140
|
+
try {
|
|
141
|
+
const agents = await listAgents();
|
|
142
|
+
setAvailableAgents(agents);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
console.error("Error fetching agents:", error);
|
|
146
|
+
setAvailableAgents([]);
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
setIsLoading(false);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
fetchAgents();
|
|
153
|
+
}, []);
|
|
154
|
+
// If still loading, show loading message
|
|
155
|
+
if (isLoading) {
|
|
156
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { children: "Loading agents..." }) }));
|
|
157
|
+
}
|
|
158
|
+
// If no agents available, show info message and auto-proceed
|
|
159
|
+
if (availableAgents.length === 0) {
|
|
160
|
+
return _jsx(NoAgentsMessage, { onNext: onNext });
|
|
161
|
+
}
|
|
162
|
+
// Show agent selection UI
|
|
163
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Register tool with agents (optional):" }) }), _jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Select which agents should have access to this tool." }), _jsx(Text, { dimColor: true, children: "You can skip this step and register agents later." })] }), _jsx(MultiSelect, { options: availableAgents.map((agent) => ({
|
|
164
|
+
label: agent,
|
|
165
|
+
value: agent,
|
|
166
|
+
})), selected: selectedAgents, onChange: onSelectedAgentsChange, onSubmit: onNext, onCancel: onBack })] }));
|
|
167
|
+
}
|
|
168
|
+
function DoneStage({ config, status, error, attachedAgents }) {
|
|
169
|
+
const { exit } = useApp();
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (status === "done") {
|
|
172
|
+
setImmediate(() => {
|
|
173
|
+
exit();
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}, [status, exit]);
|
|
177
|
+
if (status === "saving") {
|
|
178
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { children: "\u23F3 Saving tool configuration..." }) }));
|
|
179
|
+
}
|
|
180
|
+
if (status === "error") {
|
|
181
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", children: "\u274C Error saving tool" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: error }) })] }));
|
|
182
|
+
}
|
|
183
|
+
if (status === "done") {
|
|
184
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", children: "\u2705 Tool saved successfully!" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { dimColor: true, children: ["Name: ", config.name] }), _jsxs(Text, { dimColor: true, children: ["Path: ", config.path] })] }), attachedAgents.length > 0 && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsxs(Text, { dimColor: true, children: ["Registered with agents: ", attachedAgents.join(", ")] }) }))] }));
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// Main Component
|
|
190
|
+
// ============================================================================
|
|
191
|
+
function ToolStubApp({ name: initialName }) {
|
|
192
|
+
const { exit } = useApp();
|
|
193
|
+
const [stage, setStage] = useState("name");
|
|
194
|
+
const [config, setConfig] = useState({});
|
|
195
|
+
const [nameInput, setNameInput] = useState(initialName || "");
|
|
196
|
+
const [saveStatus, setSaveStatus] = useState("pending");
|
|
197
|
+
const [saveError, setSaveError] = useState(null);
|
|
198
|
+
const [selectedAgents, setSelectedAgents] = useState([]);
|
|
199
|
+
const [tempFilePath, setTempFilePath] = useState(null);
|
|
200
|
+
const [editorOpened, setEditorOpened] = useState(false);
|
|
201
|
+
// On mount, create temp file and open editor
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (editorOpened)
|
|
204
|
+
return;
|
|
205
|
+
const setupAndOpenEditor = async () => {
|
|
206
|
+
try {
|
|
207
|
+
// Generate temp file path
|
|
208
|
+
const tempFile = join(tmpdir(), `town-tool-${Date.now()}.ts`);
|
|
209
|
+
setTempFilePath(tempFile);
|
|
210
|
+
// Generate and write stub template
|
|
211
|
+
const stubContent = generateStubTemplate(initialName);
|
|
212
|
+
writeFileSync(tempFile, stubContent, "utf-8");
|
|
213
|
+
// Open in editor
|
|
214
|
+
await openInEditor(tempFile);
|
|
215
|
+
// After editor closes, move to name input stage
|
|
216
|
+
setEditorOpened(true);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
setSaveStatus("error");
|
|
220
|
+
setSaveError(`Failed to open editor: ${error instanceof Error ? error.message : String(error)}`);
|
|
221
|
+
setStage("done");
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
setupAndOpenEditor();
|
|
225
|
+
}, [initialName, editorOpened]);
|
|
226
|
+
const handleSave = (toolName, agentsToAttach) => {
|
|
227
|
+
setSaveStatus("saving");
|
|
228
|
+
try {
|
|
229
|
+
if (!tempFilePath) {
|
|
230
|
+
setSaveStatus("error");
|
|
231
|
+
setSaveError("Temp file path not found");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
// Check if temp file still exists
|
|
235
|
+
if (!existsSync(tempFilePath)) {
|
|
236
|
+
setSaveStatus("error");
|
|
237
|
+
setSaveError("Temp file was deleted");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
// Read the edited content
|
|
241
|
+
const editedContent = readFileSync(tempFilePath, "utf-8");
|
|
242
|
+
// Ensure tools directory exists
|
|
243
|
+
ensureToolsDir();
|
|
244
|
+
// Normalize the tool name for the filename
|
|
245
|
+
const normalizedName = normalizeToolName(toolName);
|
|
246
|
+
const toolsDir = getToolsDir();
|
|
247
|
+
const targetPath = join(toolsDir, `${normalizedName}.ts`);
|
|
248
|
+
// Write the edited content to the target path
|
|
249
|
+
try {
|
|
250
|
+
writeFileSync(targetPath, editedContent, "utf-8");
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
setSaveStatus("error");
|
|
254
|
+
setSaveError(`Failed to save file: ${error instanceof Error ? error.message : String(error)}`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// Create tool config
|
|
258
|
+
const toolConfig = {
|
|
259
|
+
name: toolName,
|
|
260
|
+
path: targetPath,
|
|
261
|
+
};
|
|
262
|
+
// Save to global tool storage
|
|
263
|
+
saveToolConfig(toolConfig);
|
|
264
|
+
// Update selected agents
|
|
265
|
+
for (const agentName of agentsToAttach) {
|
|
266
|
+
try {
|
|
267
|
+
const agentPath = getAgentPath(agentName);
|
|
268
|
+
const agentJsonPath = join(agentPath, "agent.json");
|
|
269
|
+
// Read existing agent.json
|
|
270
|
+
const agentJsonContent = readFileSync(agentJsonPath, "utf-8");
|
|
271
|
+
const agentDef = JSON.parse(agentJsonContent);
|
|
272
|
+
// Add tool to tools array (create array if doesn't exist)
|
|
273
|
+
if (!agentDef.tools) {
|
|
274
|
+
agentDef.tools = [];
|
|
275
|
+
}
|
|
276
|
+
// Create custom tool object
|
|
277
|
+
const customTool = {
|
|
278
|
+
type: "custom",
|
|
279
|
+
modulePath: targetPath,
|
|
280
|
+
};
|
|
281
|
+
// Check if this tool is already in the agent's config
|
|
282
|
+
const existingIndex = agentDef.tools.findIndex((tool) => typeof tool === "object" &&
|
|
283
|
+
tool !== null &&
|
|
284
|
+
"type" in tool &&
|
|
285
|
+
tool.type === "custom" &&
|
|
286
|
+
"modulePath" in tool &&
|
|
287
|
+
tool.modulePath === targetPath);
|
|
288
|
+
if (existingIndex >= 0) {
|
|
289
|
+
// Update existing config
|
|
290
|
+
agentDef.tools[existingIndex] = customTool;
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
// Add new config
|
|
294
|
+
agentDef.tools.push(customTool);
|
|
295
|
+
}
|
|
296
|
+
// Write back to agent.json
|
|
297
|
+
writeFileSync(agentJsonPath, JSON.stringify(agentDef, null, 2));
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
console.error(`Error updating agent ${agentName}:`, error);
|
|
301
|
+
// Continue with other agents even if one fails
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// Clean up temp file
|
|
305
|
+
try {
|
|
306
|
+
unlinkSync(tempFilePath);
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
// Ignore cleanup errors
|
|
310
|
+
}
|
|
311
|
+
setConfig(toolConfig);
|
|
312
|
+
setSaveStatus("done");
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
setSaveStatus("error");
|
|
316
|
+
setSaveError(error instanceof Error ? error.message : "Unknown error occurred");
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
// Wait for editor to open and close before showing UI
|
|
320
|
+
if (!editorOpened) {
|
|
321
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { children: "Opening editor..." }) }));
|
|
322
|
+
}
|
|
323
|
+
// If there was an error during editor setup, show done stage with error
|
|
324
|
+
if (saveStatus === "error" && stage === "done") {
|
|
325
|
+
return (_jsx(DoneStage, { config: config, status: saveStatus, error: saveError, attachedAgents: selectedAgents }));
|
|
326
|
+
}
|
|
327
|
+
// Name input stage
|
|
328
|
+
if (stage === "name") {
|
|
329
|
+
return (_jsx(NameInputStage, { value: nameInput, onChange: setNameInput, onNext: (name) => {
|
|
330
|
+
setConfig({ ...config, name });
|
|
331
|
+
setStage("agentSelection");
|
|
332
|
+
}, onBack: () => {
|
|
333
|
+
// Clean up temp file
|
|
334
|
+
if (tempFilePath) {
|
|
335
|
+
try {
|
|
336
|
+
unlinkSync(tempFilePath);
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
// Ignore cleanup errors
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
exit();
|
|
343
|
+
} }));
|
|
344
|
+
}
|
|
345
|
+
// Agent selection stage
|
|
346
|
+
if (stage === "agentSelection") {
|
|
347
|
+
return (_jsx(AgentSelectionStage, { selectedAgents: selectedAgents, onSelectedAgentsChange: setSelectedAgents, onNext: () => {
|
|
348
|
+
if (!config.name) {
|
|
349
|
+
setSaveStatus("error");
|
|
350
|
+
setSaveError("Tool name is required");
|
|
351
|
+
setStage("done");
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
handleSave(config.name, selectedAgents);
|
|
355
|
+
setStage("done");
|
|
356
|
+
}, onBack: () => setStage("name") }));
|
|
357
|
+
}
|
|
358
|
+
// Done stage
|
|
359
|
+
if (stage === "done") {
|
|
360
|
+
return (_jsx(DoneStage, { config: config, status: saveStatus, error: saveError, attachedAgents: selectedAgents }));
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
// ============================================================================
|
|
365
|
+
// Export and Runner
|
|
366
|
+
// ============================================================================
|
|
367
|
+
export default ToolStubApp;
|
|
368
|
+
export async function runToolStub(props = {}) {
|
|
369
|
+
// Set stdin to raw mode to capture input
|
|
370
|
+
if (process.stdin.isTTY) {
|
|
371
|
+
process.stdin.setRawMode(true);
|
|
372
|
+
}
|
|
373
|
+
const { waitUntilExit } = render(_jsx(ToolStubApp, { ...props }));
|
|
374
|
+
// Wait for the app to exit before returning
|
|
375
|
+
await waitUntilExit();
|
|
376
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
import { argument, command, constant, flag, multiple, object, option, optional, or, } from "@optique/core";
|
|
3
4
|
import { message } from "@optique/core/message";
|
|
4
5
|
import { integer, string } from "@optique/core/valueparser";
|
|
5
6
|
import { run } from "@optique/run";
|
|
7
|
+
import { isInsideTownProject } from "@townco/agent/storage";
|
|
6
8
|
import { createSecret, deleteSecret, genenv, listSecrets, } from "@townco/secret";
|
|
7
9
|
import inquirer from "inquirer";
|
|
8
10
|
import { match } from "ts-pattern";
|
|
9
11
|
import { configureCommand } from "./commands/configure.js";
|
|
10
12
|
import { createCommand } from "./commands/create.js";
|
|
13
|
+
import { createProjectCommand } from "./commands/create-project.js";
|
|
11
14
|
import { deleteCommand } from "./commands/delete.js";
|
|
12
15
|
import { editCommand } from "./commands/edit.js";
|
|
13
16
|
import { listCommand } from "./commands/list.js";
|
|
@@ -41,7 +44,8 @@ const parser = or(command("deploy", constant("deploy"), { brief: message `Deploy
|
|
|
41
44
|
model: optional(option("-m", "--model", string())),
|
|
42
45
|
tools: multiple(option("-t", "--tool", string())),
|
|
43
46
|
systemPrompt: optional(option("-p", "--prompt", string())),
|
|
44
|
-
|
|
47
|
+
init: optional(option("--init", string())),
|
|
48
|
+
}), { brief: message `Create a new agent or project (with --init <path>).` }), command("list", constant("list"), { brief: message `List all agents.` }), command("run", object({
|
|
45
49
|
command: constant("run"),
|
|
46
50
|
name: argument(string({ metavar: "NAME" })),
|
|
47
51
|
http: optional(flag("--http")),
|
|
@@ -109,15 +113,59 @@ async function main(parser, meta) {
|
|
|
109
113
|
.with("configure", async () => {
|
|
110
114
|
await configureCommand();
|
|
111
115
|
})
|
|
112
|
-
.with({ command: "create" }, async ({ name, model, tools, systemPrompt }) => {
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
116
|
+
.with({ command: "create" }, async ({ name, model, tools, systemPrompt, init }) => {
|
|
117
|
+
// Check if --init flag is present for project scaffolding
|
|
118
|
+
if (init !== null && init !== undefined) {
|
|
119
|
+
// Project mode - scaffold a standalone project
|
|
120
|
+
await createProjectCommand({
|
|
121
|
+
path: init,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
// Check if we're inside a Town project
|
|
126
|
+
const projectRoot = await isInsideTownProject();
|
|
127
|
+
if (projectRoot === null) {
|
|
128
|
+
// Not in a project - prompt user to initialize
|
|
129
|
+
const answer = await inquirer.prompt([
|
|
130
|
+
{
|
|
131
|
+
type: "confirm",
|
|
132
|
+
name: "initProject",
|
|
133
|
+
message: "Not inside a Town project. Initialize project in current directory?",
|
|
134
|
+
default: true,
|
|
135
|
+
},
|
|
136
|
+
]);
|
|
137
|
+
if (answer.initProject) {
|
|
138
|
+
// Initialize project first
|
|
139
|
+
await createProjectCommand({ path: process.cwd() });
|
|
140
|
+
// Then create agent
|
|
141
|
+
await createCommand({
|
|
142
|
+
...(name !== undefined && { name }),
|
|
143
|
+
...(model !== undefined && { model }),
|
|
144
|
+
...(tools.length > 0 && { tools }),
|
|
145
|
+
...(systemPrompt !== undefined && { systemPrompt }),
|
|
146
|
+
agentsDir: join(process.cwd(), "agents"),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// User declined
|
|
151
|
+
console.log("\nPlease run 'town create' inside a project directory, or run:\n" +
|
|
152
|
+
" town create --init <path>\n" +
|
|
153
|
+
"to create a project.");
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
// Agent mode - create agent in existing project
|
|
159
|
+
// Create command starts a long-running Ink session
|
|
160
|
+
await createCommand({
|
|
161
|
+
...(name !== undefined && { name }),
|
|
162
|
+
...(model !== undefined && { model }),
|
|
163
|
+
...(tools.length > 0 && { tools }),
|
|
164
|
+
...(systemPrompt !== undefined && { systemPrompt }),
|
|
165
|
+
agentsDir: join(projectRoot, "agents"),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
121
169
|
})
|
|
122
170
|
.with("list", async () => {
|
|
123
171
|
await listCommand();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@townco/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"town": "./dist/index.js"
|
|
@@ -18,16 +18,16 @@
|
|
|
18
18
|
"build": "tsc"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"@townco/tsconfig": "0.1.
|
|
21
|
+
"@townco/tsconfig": "0.1.15",
|
|
22
22
|
"@types/bun": "^1.3.1",
|
|
23
23
|
"@types/react": "^19.2.2"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@optique/core": "^0.6.2",
|
|
27
27
|
"@optique/run": "^0.6.2",
|
|
28
|
-
"@townco/agent": "0.1.
|
|
29
|
-
"@townco/secret": "0.1.
|
|
30
|
-
"@townco/ui": "0.1.
|
|
28
|
+
"@townco/agent": "0.1.23",
|
|
29
|
+
"@townco/secret": "0.1.18",
|
|
30
|
+
"@townco/ui": "0.1.18",
|
|
31
31
|
"@types/inquirer": "^9.0.9",
|
|
32
32
|
"ink": "^6.4.0",
|
|
33
33
|
"ink-text-input": "^6.0.0",
|