@townco/cli 0.1.0
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 +9 -0
- package/dist/commands/create.js +385 -0
- package/dist/commands/delete.d.ts +1 -0
- package/dist/commands/delete.js +60 -0
- package/dist/commands/edit.d.ts +1 -0
- package/dist/commands/edit.js +82 -0
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +55 -0
- package/dist/commands/run.d.ts +7 -0
- package/dist/commands/run.js +129 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +135 -0
- package/package.json +44 -0
|
@@ -0,0 +1,385 @@
|
|
|
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
|
+
import { scaffoldAgent } from "@townco/agent/scaffold";
|
|
7
|
+
import { InputBox, MultiSelect, SingleSelect } from "@townco/ui/tui";
|
|
8
|
+
import { Box, render, Text, useInput } from "ink";
|
|
9
|
+
import TextInput from "ink-text-input";
|
|
10
|
+
import { useEffect, useState } from "react";
|
|
11
|
+
const AVAILABLE_MODELS = [
|
|
12
|
+
{
|
|
13
|
+
label: "Claude Sonnet 4.5",
|
|
14
|
+
value: "claude-sonnet-4-5-20250929",
|
|
15
|
+
description: "Latest Sonnet model - balanced performance and speed",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
label: "Claude Sonnet 4",
|
|
19
|
+
value: "claude-sonnet-4-20250514",
|
|
20
|
+
description: "Previous Sonnet version",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
label: "Claude Opus 4",
|
|
24
|
+
value: "claude-opus-4-20250514",
|
|
25
|
+
description: "Most capable model - best for complex tasks",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
label: "Claude 3.5 Sonnet",
|
|
29
|
+
value: "claude-3-5-sonnet-20241022",
|
|
30
|
+
description: "Claude 3.5 generation Sonnet",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
label: "Claude 3.5 Haiku",
|
|
34
|
+
value: "claude-3-5-haiku-20241022",
|
|
35
|
+
description: "Fastest model - great for simple tasks",
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
const AVAILABLE_TOOLS = [
|
|
39
|
+
{
|
|
40
|
+
label: "Todo Write",
|
|
41
|
+
value: "todo_write",
|
|
42
|
+
description: "Task management and planning tool",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
label: "Web Search",
|
|
46
|
+
value: "web_search",
|
|
47
|
+
description: "Exa-powered web search (requires EXA_API_KEY)",
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
function NameInput({ nameInput, setNameInput, onSubmit }) {
|
|
51
|
+
useInput((_input, key) => {
|
|
52
|
+
if (key.escape) {
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
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
|
+
}
|
|
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
|
+
function CreateApp({ name: initialName, model: initialModel, tools: initialTools, systemPrompt: initialSystemPrompt, overwrite = false, }) {
|
|
123
|
+
// Determine the starting stage based on what's provided
|
|
124
|
+
const determineInitialStage = () => {
|
|
125
|
+
if (!initialName)
|
|
126
|
+
return "name";
|
|
127
|
+
if (!initialModel)
|
|
128
|
+
return "model";
|
|
129
|
+
if (!initialTools || initialTools.length === 0)
|
|
130
|
+
return "tools";
|
|
131
|
+
if (!initialSystemPrompt)
|
|
132
|
+
return "systemPrompt";
|
|
133
|
+
return "review";
|
|
134
|
+
};
|
|
135
|
+
const [stage, setStage] = useState(determineInitialStage());
|
|
136
|
+
const [agentDef, setAgentDef] = useState({
|
|
137
|
+
name: initialName || "",
|
|
138
|
+
model: initialModel || "",
|
|
139
|
+
tools: initialTools ? [...initialTools] : [],
|
|
140
|
+
systemPrompt: initialSystemPrompt || "",
|
|
141
|
+
});
|
|
142
|
+
const [nameInput, setNameInput] = useState(initialName || "");
|
|
143
|
+
const [systemPromptInput, setSystemPromptInput] = useState(initialSystemPrompt || "");
|
|
144
|
+
const [isEditingPrompt, setIsEditingPrompt] = useState(false);
|
|
145
|
+
const [promptEditMode, setPromptEditMode] = useState(null);
|
|
146
|
+
const [reviewSelection, setReviewSelection] = useState(null);
|
|
147
|
+
const [isEditingFromReview, setIsEditingFromReview] = useState(false);
|
|
148
|
+
const [scaffoldStatus, setScaffoldStatus] = useState("pending");
|
|
149
|
+
const [scaffoldError, setScaffoldError] = useState(null);
|
|
150
|
+
const [agentPath, setAgentPath] = useState(null);
|
|
151
|
+
// Handle opening editor when systemPrompt stage is entered from review
|
|
152
|
+
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) => {
|
|
160
|
+
if (editorContent !== null) {
|
|
161
|
+
// Editor worked
|
|
162
|
+
setAgentDef({ ...agentDef, systemPrompt: editorContent });
|
|
163
|
+
setIsEditingPrompt(false);
|
|
164
|
+
setIsEditingFromReview(false);
|
|
165
|
+
setStage("review");
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
// Fallback to inline
|
|
169
|
+
setPromptEditMode("inline");
|
|
170
|
+
setIsEditingPrompt(false);
|
|
171
|
+
setSystemPromptInput(agentDef.systemPrompt || "You are a helpful assistant.");
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}, [stage, isEditingFromReview, isEditingPrompt, promptEditMode, agentDef]);
|
|
176
|
+
// Handle scaffolding when entering "done" stage
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (stage === "done" && scaffoldStatus === "pending") {
|
|
179
|
+
setScaffoldStatus("scaffolding");
|
|
180
|
+
// Scaffold the agent
|
|
181
|
+
// At this point, all required fields should be present
|
|
182
|
+
if (!agentDef.name || !agentDef.model) {
|
|
183
|
+
setScaffoldStatus("error");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const name = agentDef.name;
|
|
187
|
+
const model = agentDef.model;
|
|
188
|
+
scaffoldAgent({
|
|
189
|
+
name,
|
|
190
|
+
definition: {
|
|
191
|
+
model,
|
|
192
|
+
systemPrompt: agentDef.systemPrompt || null,
|
|
193
|
+
tools: agentDef.tools || [],
|
|
194
|
+
},
|
|
195
|
+
overwrite,
|
|
196
|
+
}).then((result) => {
|
|
197
|
+
if (result.success) {
|
|
198
|
+
setScaffoldStatus("done");
|
|
199
|
+
setAgentPath(result.path);
|
|
200
|
+
// Output in the same format as 'town list'
|
|
201
|
+
const modelLabel = model.replace("claude-", "");
|
|
202
|
+
console.log(`\n\x1b[1mAgent created successfully!\x1b[0m\n`);
|
|
203
|
+
console.log(` \x1b[32m●\x1b[0m \x1b[1m${name}\x1b[0m`);
|
|
204
|
+
console.log(` \x1b[2mModel: ${modelLabel}\x1b[0m`);
|
|
205
|
+
if (agentDef.tools && agentDef.tools.length > 0) {
|
|
206
|
+
console.log(` \x1b[2mTools: ${agentDef.tools.join(", ")}\x1b[0m`);
|
|
207
|
+
}
|
|
208
|
+
console.log(` \x1b[2mPath: ${result.path}\x1b[0m`);
|
|
209
|
+
console.log();
|
|
210
|
+
console.log(`\x1b[2mRun an agent with: town run ${agentDef.name}\x1b[0m`);
|
|
211
|
+
console.log(`\x1b[2mTUI mode (default), --gui for web interface, --http for API server\x1b[0m`);
|
|
212
|
+
// Exit immediately
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
setScaffoldStatus("error");
|
|
217
|
+
setScaffoldError(result.error || "Unknown error occurred");
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}, [stage, scaffoldStatus, agentDef, overwrite]);
|
|
222
|
+
// Name stage
|
|
223
|
+
if (stage === "name") {
|
|
224
|
+
return (_jsx(NameInput, { nameInput: nameInput, setNameInput: setNameInput, onSubmit: () => {
|
|
225
|
+
if (nameInput.trim()) {
|
|
226
|
+
setAgentDef({ ...agentDef, name: nameInput.trim() });
|
|
227
|
+
if (isEditingFromReview) {
|
|
228
|
+
setIsEditingFromReview(false);
|
|
229
|
+
setStage("review");
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
setStage("model");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} }));
|
|
236
|
+
}
|
|
237
|
+
// Model selection stage
|
|
238
|
+
if (stage === "model") {
|
|
239
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, children: ["Select model for agent: ", agentDef.name] }) }), _jsx(SingleSelect, { options: AVAILABLE_MODELS, selected: agentDef.model || null, onChange: (model) => setAgentDef({ ...agentDef, model }), onSubmit: (model) => {
|
|
240
|
+
setAgentDef({ ...agentDef, model });
|
|
241
|
+
if (isEditingFromReview) {
|
|
242
|
+
setIsEditingFromReview(false);
|
|
243
|
+
setStage("review");
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
setStage("tools");
|
|
247
|
+
}
|
|
248
|
+
}, onCancel: () => {
|
|
249
|
+
if (initialName) {
|
|
250
|
+
process.exit(0);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
setStage("name");
|
|
254
|
+
}
|
|
255
|
+
} })] }));
|
|
256
|
+
}
|
|
257
|
+
// Tools selection stage
|
|
258
|
+
if (stage === "tools") {
|
|
259
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, children: ["Select tools for agent: ", agentDef.name] }) }), _jsx(MultiSelect, { options: AVAILABLE_TOOLS, selected: agentDef.tools || [], onChange: (tools) => setAgentDef({ ...agentDef, tools }), onSubmit: async () => {
|
|
260
|
+
// If editing from review, just go back to review
|
|
261
|
+
if (isEditingFromReview) {
|
|
262
|
+
setIsEditingFromReview(false);
|
|
263
|
+
setStage("review");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
// If systemPrompt was provided via flag, skip to review
|
|
267
|
+
if (initialSystemPrompt) {
|
|
268
|
+
setStage("review");
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
setStage("systemPrompt");
|
|
272
|
+
// Attempt to open editor
|
|
273
|
+
setIsEditingPrompt(true);
|
|
274
|
+
const editorContent = await openEditor(agentDef.systemPrompt || "You are a helpful assistant.");
|
|
275
|
+
if (editorContent !== null) {
|
|
276
|
+
// Editor worked
|
|
277
|
+
setAgentDef({ ...agentDef, systemPrompt: editorContent });
|
|
278
|
+
setIsEditingPrompt(false);
|
|
279
|
+
setStage("review");
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
// Fallback to inline
|
|
283
|
+
setPromptEditMode("inline");
|
|
284
|
+
setIsEditingPrompt(false);
|
|
285
|
+
setSystemPromptInput(agentDef.systemPrompt || "You are a helpful assistant.");
|
|
286
|
+
}
|
|
287
|
+
}, onCancel: () => setStage("model") })] }));
|
|
288
|
+
}
|
|
289
|
+
// System prompt stage (inline fallback)
|
|
290
|
+
if (stage === "systemPrompt") {
|
|
291
|
+
if (isEditingPrompt) {
|
|
292
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Opening editor for system prompt..." }) }), _jsx(Text, { dimColor: true, children: "You can edit the system prompt in your preferred editor." })] }));
|
|
293
|
+
}
|
|
294
|
+
if (promptEditMode === "inline") {
|
|
295
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Enter system prompt (multi-line supported):" }) }), _jsx(InputBox, { value: systemPromptInput, onChange: setSystemPromptInput, onSubmit: () => {
|
|
296
|
+
setAgentDef({ ...agentDef, systemPrompt: systemPromptInput });
|
|
297
|
+
if (isEditingFromReview) {
|
|
298
|
+
setIsEditingFromReview(false);
|
|
299
|
+
setPromptEditMode(null);
|
|
300
|
+
}
|
|
301
|
+
setStage("review");
|
|
302
|
+
}, onEscape: () => {
|
|
303
|
+
if (isEditingFromReview) {
|
|
304
|
+
setIsEditingFromReview(false);
|
|
305
|
+
setPromptEditMode(null);
|
|
306
|
+
setStage("review");
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
setStage("tools");
|
|
310
|
+
}
|
|
311
|
+
}, isSubmitting: false, attachedFiles: [] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: Continue \u2022 Esc: Back" }) })] }));
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
// Review stage
|
|
316
|
+
if (stage === "review") {
|
|
317
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Review Agent Configuration:" }) }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Name: " }), agentDef.name] }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Model: " }), agentDef.model] }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Tools: " }), agentDef.tools && agentDef.tools.length > 0
|
|
318
|
+
? agentDef.tools.join(", ")
|
|
319
|
+
: "none"] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, children: "System Prompt:" }) }), _jsx(Box, { paddingLeft: 2, flexDirection: "column", children: agentDef.systemPrompt?.split("\n").map((line) => (_jsx(Text, { dimColor: true, children: line || " " }, line))) })] }), _jsx(SingleSelect, { options: [
|
|
320
|
+
{
|
|
321
|
+
label: "Looks good, continue",
|
|
322
|
+
value: "continue",
|
|
323
|
+
description: "Proceed to next step",
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
label: "Edit name",
|
|
327
|
+
value: "name",
|
|
328
|
+
description: "Go back and change the agent name",
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
label: "Edit model",
|
|
332
|
+
value: "model",
|
|
333
|
+
description: "Go back and change the model",
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
label: "Edit tools",
|
|
337
|
+
value: "tools",
|
|
338
|
+
description: "Go back and change tool selection",
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
label: "Edit system prompt",
|
|
342
|
+
value: "systemPrompt",
|
|
343
|
+
description: "Go back and change the system prompt",
|
|
344
|
+
},
|
|
345
|
+
], selected: reviewSelection, onChange: (value) => {
|
|
346
|
+
setReviewSelection(value);
|
|
347
|
+
}, onSubmit: (value) => {
|
|
348
|
+
// Navigate based on selection
|
|
349
|
+
setReviewSelection(value);
|
|
350
|
+
if (value === "continue") {
|
|
351
|
+
setStage("done");
|
|
352
|
+
}
|
|
353
|
+
else if (value === "name" ||
|
|
354
|
+
value === "model" ||
|
|
355
|
+
value === "tools" ||
|
|
356
|
+
value === "systemPrompt") {
|
|
357
|
+
// Set flag so we return to review after editing
|
|
358
|
+
setIsEditingFromReview(true);
|
|
359
|
+
setStage(value);
|
|
360
|
+
}
|
|
361
|
+
}, onCancel: () => setStage("tools") })] }));
|
|
362
|
+
}
|
|
363
|
+
// Done stage
|
|
364
|
+
if (stage === "done") {
|
|
365
|
+
if (scaffoldStatus === "scaffolding") {
|
|
366
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { children: "\u23F3 Creating agent package..." }) }));
|
|
367
|
+
}
|
|
368
|
+
if (scaffoldStatus === "error") {
|
|
369
|
+
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 }) })] }));
|
|
370
|
+
}
|
|
371
|
+
if (scaffoldStatus === "done") {
|
|
372
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", children: "\u2713 Agent package created successfully!" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Path: ", agentPath] }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["You can run the agent with:", " ", _jsxs(Text, { bold: true, color: "cyan", children: ["town run ", agentDef.name] })] }) })] }));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
export async function createCommand(props) {
|
|
378
|
+
// Set stdin to raw mode to capture input
|
|
379
|
+
if (process.stdin.isTTY) {
|
|
380
|
+
process.stdin.setRawMode(true);
|
|
381
|
+
}
|
|
382
|
+
const { waitUntilExit } = render(_jsx(CreateApp, { ...props }));
|
|
383
|
+
// Wait for the app to exit before returning
|
|
384
|
+
await waitUntilExit();
|
|
385
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function deleteCommand(name: string): Promise<void>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { agentExists, deleteAgent } from "@townco/agent/storage";
|
|
3
|
+
import { SingleSelect } from "@townco/ui/tui";
|
|
4
|
+
import { Box, render, Text } from "ink";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
function DeleteApp({ name }) {
|
|
7
|
+
const [confirmed, setConfirmed] = useState(null);
|
|
8
|
+
if (confirmed === null) {
|
|
9
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, children: ["Are you sure you want to delete agent", " ", _jsx(Text, { color: "red", children: name }), "?"] }) }), _jsx(Text, { dimColor: true, children: "This action cannot be undone." }), _jsx(Box, { marginTop: 1, children: _jsx(SingleSelect, { options: [
|
|
10
|
+
{
|
|
11
|
+
label: "Yes, delete",
|
|
12
|
+
value: "yes",
|
|
13
|
+
description: "Permanently delete this agent",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
label: "No, cancel",
|
|
17
|
+
value: "no",
|
|
18
|
+
description: "Keep the agent",
|
|
19
|
+
},
|
|
20
|
+
], selected: null, onChange: () => { }, onSubmit: async (value) => {
|
|
21
|
+
if (value === "yes") {
|
|
22
|
+
setConfirmed(true);
|
|
23
|
+
try {
|
|
24
|
+
await deleteAgent(name);
|
|
25
|
+
setTimeout(() => process.exit(0), 1500);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.error(`Error deleting agent: ${error instanceof Error ? error.message : String(error)}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
setConfirmed(false);
|
|
34
|
+
setTimeout(() => process.exit(0), 500);
|
|
35
|
+
}
|
|
36
|
+
}, onCancel: () => {
|
|
37
|
+
setConfirmed(false);
|
|
38
|
+
setTimeout(() => process.exit(0), 500);
|
|
39
|
+
} }) })] }));
|
|
40
|
+
}
|
|
41
|
+
if (confirmed) {
|
|
42
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: "green", children: "\u2713 Agent deleted successfully" }) }));
|
|
43
|
+
}
|
|
44
|
+
return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "Cancelled" }) }));
|
|
45
|
+
}
|
|
46
|
+
export async function deleteCommand(name) {
|
|
47
|
+
// Check if agent exists
|
|
48
|
+
const exists = await agentExists(name);
|
|
49
|
+
if (!exists) {
|
|
50
|
+
console.error(`Error: Agent "${name}" not found.`);
|
|
51
|
+
console.log('\nList agents with "town list"');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
// Set stdin to raw mode to capture input
|
|
55
|
+
if (process.stdin.isTTY) {
|
|
56
|
+
process.stdin.setRawMode(true);
|
|
57
|
+
}
|
|
58
|
+
const { waitUntilExit } = render(_jsx(DeleteApp, { name: name }));
|
|
59
|
+
await waitUntilExit();
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function editCommand(name: string): Promise<void>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { agentExists, getAgentPath } from "@townco/agent/storage";
|
|
4
|
+
import { createCommand } from "./create.js";
|
|
5
|
+
async function loadAgentConfig(name) {
|
|
6
|
+
try {
|
|
7
|
+
const agentPath = getAgentPath(name);
|
|
8
|
+
const indexPath = join(agentPath, "index.ts");
|
|
9
|
+
const content = await readFile(indexPath, "utf-8");
|
|
10
|
+
// Parse model
|
|
11
|
+
const modelMatch = content.match(/model:\s*"([^"]+)"/);
|
|
12
|
+
if (!modelMatch) {
|
|
13
|
+
throw new Error("Failed to parse model from agent configuration");
|
|
14
|
+
}
|
|
15
|
+
const model = modelMatch[1];
|
|
16
|
+
// Parse tools
|
|
17
|
+
const toolsMatch = content.match(/tools:\s*\[([^\]]*)\]/);
|
|
18
|
+
let tools = [];
|
|
19
|
+
if (toolsMatch?.[1]) {
|
|
20
|
+
const toolsStr = toolsMatch[1];
|
|
21
|
+
tools = toolsStr
|
|
22
|
+
.split(",")
|
|
23
|
+
.map((t) => t.trim().replace(/["']/g, ""))
|
|
24
|
+
.filter((t) => t.length > 0);
|
|
25
|
+
}
|
|
26
|
+
// Parse systemPrompt - handle multiline strings and null
|
|
27
|
+
const systemPromptMatch = content.match(/systemPrompt:\s*(null|"([^"]*)"|`([^`]*)`)/s);
|
|
28
|
+
let systemPrompt;
|
|
29
|
+
if (systemPromptMatch) {
|
|
30
|
+
if (systemPromptMatch[1] === "null") {
|
|
31
|
+
systemPrompt = "";
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
// Get either double-quoted (group 2) or backtick-quoted (group 3) content
|
|
35
|
+
systemPrompt = systemPromptMatch[2] || systemPromptMatch[3] || "";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (systemPrompt !== undefined && systemPrompt !== "") {
|
|
39
|
+
return {
|
|
40
|
+
name,
|
|
41
|
+
model: model,
|
|
42
|
+
tools,
|
|
43
|
+
systemPrompt,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
name,
|
|
48
|
+
model: model,
|
|
49
|
+
tools,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.error(`Error loading agent config: ${error instanceof Error ? error.message : String(error)}`);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function editCommand(name) {
|
|
58
|
+
// Check if agent exists
|
|
59
|
+
const exists = await agentExists(name);
|
|
60
|
+
if (!exists) {
|
|
61
|
+
console.error(`Error: Agent "${name}" not found.`);
|
|
62
|
+
console.log('\nCreate an agent with "town create" or list agents with "town list"');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
// Load existing config
|
|
66
|
+
const config = await loadAgentConfig(name);
|
|
67
|
+
if (!config) {
|
|
68
|
+
console.error(`Error: Failed to load agent configuration for "${name}".`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
console.log(`Editing agent: ${name}\n`);
|
|
72
|
+
// Reuse create command with existing values and overwrite flag
|
|
73
|
+
await createCommand({
|
|
74
|
+
name: config.name,
|
|
75
|
+
model: config.model,
|
|
76
|
+
tools: config.tools,
|
|
77
|
+
...(config.systemPrompt !== undefined && {
|
|
78
|
+
systemPrompt: config.systemPrompt,
|
|
79
|
+
}),
|
|
80
|
+
overwrite: true,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function listCommand(): Promise<void>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getAgentPath, listAgents } from "@townco/agent/storage";
|
|
4
|
+
async function getAgentInfo(name) {
|
|
5
|
+
try {
|
|
6
|
+
const agentPath = getAgentPath(name);
|
|
7
|
+
const configPath = join(agentPath, "agent.json");
|
|
8
|
+
const content = await readFile(configPath, "utf-8");
|
|
9
|
+
const config = JSON.parse(content);
|
|
10
|
+
return {
|
|
11
|
+
name,
|
|
12
|
+
model: config.model,
|
|
13
|
+
tools: config.tools || [],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
return {
|
|
18
|
+
name,
|
|
19
|
+
error: error instanceof Error ? error.message : "Failed to read agent",
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function listCommand() {
|
|
24
|
+
const agents = await listAgents();
|
|
25
|
+
if (agents.length === 0) {
|
|
26
|
+
console.log("No agents found.");
|
|
27
|
+
console.log('\x1b[2mCreate your first agent with "town create"\x1b[0m');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
console.log(`\x1b[1mFound ${agents.length} agent${agents.length === 1 ? "" : "s"}:\x1b[0m\n`);
|
|
31
|
+
// Get info for all agents
|
|
32
|
+
const agentInfos = await Promise.all(agents.map((name) => getAgentInfo(name)));
|
|
33
|
+
// Display each agent
|
|
34
|
+
for (const info of agentInfos) {
|
|
35
|
+
const agentPath = getAgentPath(info.name);
|
|
36
|
+
if (info.error) {
|
|
37
|
+
console.log(` \x1b[31m✗\x1b[0m \x1b[1m${info.name}\x1b[0m`);
|
|
38
|
+
console.log(` \x1b[2mError: ${info.error}\x1b[0m`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const modelLabel = info.model
|
|
42
|
+
? info.model.replace("claude-", "")
|
|
43
|
+
: "unknown";
|
|
44
|
+
console.log(` \x1b[32m●\x1b[0m \x1b[1m${info.name}\x1b[0m`);
|
|
45
|
+
console.log(` \x1b[2mModel: ${modelLabel}\x1b[0m`);
|
|
46
|
+
if (info.tools && info.tools.length > 0) {
|
|
47
|
+
console.log(` \x1b[2mTools: ${info.tools.join(", ")}\x1b[0m`);
|
|
48
|
+
}
|
|
49
|
+
console.log(` \x1b[2mPath: ${agentPath}\x1b[0m`);
|
|
50
|
+
}
|
|
51
|
+
console.log(); // Empty line between agents
|
|
52
|
+
}
|
|
53
|
+
console.log(`\x1b[2mRun an agent with: town run <name>\x1b[0m`);
|
|
54
|
+
console.log(`\x1b[2mTUI mode (default), --gui for web interface, --http for API server\x1b[0m`);
|
|
55
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { agentExists, getAgentPath } from "@townco/agent/storage";
|
|
4
|
+
export async function runCommand(options) {
|
|
5
|
+
const { name, http = false, gui = false, port = 3100 } = options;
|
|
6
|
+
// Check if agent exists
|
|
7
|
+
const exists = await agentExists(name);
|
|
8
|
+
if (!exists) {
|
|
9
|
+
console.error(`Error: Agent "${name}" not found.`);
|
|
10
|
+
console.log('\nCreate an agent with "town create" or list agents with "town list"');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
const agentPath = getAgentPath(name);
|
|
14
|
+
const binPath = join(agentPath, "bin.ts");
|
|
15
|
+
// If GUI or HTTP-only mode, run differently
|
|
16
|
+
if (gui) {
|
|
17
|
+
const guiPath = join(agentPath, "gui");
|
|
18
|
+
// Check if GUI exists
|
|
19
|
+
try {
|
|
20
|
+
const { stat } = await import("node:fs/promises");
|
|
21
|
+
await stat(guiPath);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
console.error(`Error: GUI not found for agent "${name}".`);
|
|
25
|
+
console.log(`\nThe GUI was not bundled with this agent.`);
|
|
26
|
+
console.log(`Recreate the agent with "town create" to include the GUI.`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
console.log(`Starting agent "${name}" with GUI...`);
|
|
30
|
+
console.log(`Starting agent HTTP server on port ${port}...`);
|
|
31
|
+
console.log(`Starting GUI development server...\n`);
|
|
32
|
+
// Start the agent in HTTP mode first
|
|
33
|
+
const agentProcess = spawn("bun", [binPath, "http"], {
|
|
34
|
+
cwd: agentPath,
|
|
35
|
+
stdio: "pipe",
|
|
36
|
+
env: {
|
|
37
|
+
...process.env,
|
|
38
|
+
NODE_ENV: process.env.NODE_ENV || "production",
|
|
39
|
+
PORT: port.toString(),
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
agentProcess.on("error", (error) => {
|
|
43
|
+
console.error(`Failed to start agent: ${error.message}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
});
|
|
46
|
+
// Wait a bit for agent to start, then start the GUI
|
|
47
|
+
setTimeout(() => {
|
|
48
|
+
// Start the GUI dev server
|
|
49
|
+
const guiProcess = spawn("bun", ["run", "dev"], {
|
|
50
|
+
cwd: guiPath,
|
|
51
|
+
stdio: "inherit",
|
|
52
|
+
env: {
|
|
53
|
+
...process.env,
|
|
54
|
+
VITE_AGENT_URL: `http://localhost:${port}`,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
guiProcess.on("error", (error) => {
|
|
58
|
+
console.error(`Failed to start GUI: ${error.message}`);
|
|
59
|
+
agentProcess.kill();
|
|
60
|
+
process.exit(1);
|
|
61
|
+
});
|
|
62
|
+
guiProcess.on("close", (code) => {
|
|
63
|
+
agentProcess.kill();
|
|
64
|
+
if (code !== 0 && code !== null) {
|
|
65
|
+
console.error(`GUI exited with code ${code}`);
|
|
66
|
+
process.exit(code);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}, 1000);
|
|
70
|
+
agentProcess.on("close", (code) => {
|
|
71
|
+
if (code !== 0 && code !== null) {
|
|
72
|
+
console.error(`Agent exited with code ${code}`);
|
|
73
|
+
process.exit(code);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
else if (http) {
|
|
79
|
+
console.log(`Starting agent "${name}" in HTTP mode on port ${port}...`);
|
|
80
|
+
console.log(`\nEndpoints:`);
|
|
81
|
+
console.log(` http://localhost:${port}/health - Health check`);
|
|
82
|
+
console.log(` http://localhost:${port}/rpc - RPC endpoint`);
|
|
83
|
+
console.log(` http://localhost:${port}/events - SSE event stream\n`);
|
|
84
|
+
// Run the agent in HTTP mode
|
|
85
|
+
const agentProcess = spawn("bun", [binPath, "http"], {
|
|
86
|
+
cwd: agentPath,
|
|
87
|
+
stdio: "inherit",
|
|
88
|
+
env: {
|
|
89
|
+
...process.env,
|
|
90
|
+
NODE_ENV: process.env.NODE_ENV || "production",
|
|
91
|
+
PORT: port.toString(),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
agentProcess.on("error", (error) => {
|
|
95
|
+
console.error(`Failed to start agent: ${error.message}`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
});
|
|
98
|
+
agentProcess.on("close", (code) => {
|
|
99
|
+
if (code !== 0 && code !== null) {
|
|
100
|
+
console.error(`Agent exited with code ${code}`);
|
|
101
|
+
process.exit(code);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Default: Start TUI interface with the agent
|
|
107
|
+
console.log(`Starting interactive terminal for agent "${name}"...\n`);
|
|
108
|
+
// Get path to TUI app
|
|
109
|
+
const tuiPath = join(process.cwd(), "apps", "tui", "src", "index.tsx");
|
|
110
|
+
// Run TUI with the agent
|
|
111
|
+
const tuiProcess = spawn("bun", [tuiPath, "--agent", binPath], {
|
|
112
|
+
cwd: agentPath,
|
|
113
|
+
stdio: "inherit",
|
|
114
|
+
env: {
|
|
115
|
+
...process.env,
|
|
116
|
+
NODE_ENV: process.env.NODE_ENV || "production",
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
tuiProcess.on("error", (error) => {
|
|
120
|
+
console.error(`Failed to start TUI: ${error.message}`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
});
|
|
123
|
+
tuiProcess.on("close", (code) => {
|
|
124
|
+
if (code !== 0 && code !== null) {
|
|
125
|
+
console.error(`TUI exited with code ${code}`);
|
|
126
|
+
process.exit(code);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { argument, command, constant, flag, multiple, object, option, optional, or, } from "@optique/core";
|
|
3
|
+
import { message } from "@optique/core/message";
|
|
4
|
+
import { integer, string } from "@optique/core/valueparser";
|
|
5
|
+
import { run } from "@optique/run";
|
|
6
|
+
import { createSecret, deleteSecret, genenv, listSecrets } from "@townco/secret";
|
|
7
|
+
import inquirer from "inquirer";
|
|
8
|
+
import { match } from "ts-pattern";
|
|
9
|
+
import { createCommand } from "./commands/create.js";
|
|
10
|
+
import { deleteCommand } from "./commands/delete.js";
|
|
11
|
+
import { editCommand } from "./commands/edit.js";
|
|
12
|
+
import { listCommand } from "./commands/list.js";
|
|
13
|
+
import { runCommand } from "./commands/run.js";
|
|
14
|
+
/**
|
|
15
|
+
* Securely prompt for a secret value without echoing to the terminal
|
|
16
|
+
*/
|
|
17
|
+
async function promptSecret(secretName) {
|
|
18
|
+
const answers = await inquirer.prompt([
|
|
19
|
+
{
|
|
20
|
+
type: "password",
|
|
21
|
+
name: "value",
|
|
22
|
+
message: `Enter value for secret '${secretName}':`,
|
|
23
|
+
mask: "*",
|
|
24
|
+
},
|
|
25
|
+
]);
|
|
26
|
+
return answers.value;
|
|
27
|
+
}
|
|
28
|
+
const parser = or(command("deploy", constant("deploy"), { brief: message `Deploy a Town.` }), command("create", object({
|
|
29
|
+
command: constant("create"),
|
|
30
|
+
name: optional(option("-n", "--name", string())),
|
|
31
|
+
model: optional(option("-m", "--model", string())),
|
|
32
|
+
tools: multiple(option("-t", "--tool", string())),
|
|
33
|
+
systemPrompt: optional(option("-p", "--prompt", string())),
|
|
34
|
+
}), { brief: message `Create a new agent.` }), command("list", constant("list"), { brief: message `List all agents.` }), command("run", object({
|
|
35
|
+
command: constant("run"),
|
|
36
|
+
name: argument(string({ metavar: "NAME" })),
|
|
37
|
+
http: optional(flag("--http")),
|
|
38
|
+
gui: optional(flag("--gui")),
|
|
39
|
+
port: optional(option("-p", "--port", integer())),
|
|
40
|
+
}), { brief: message `Run an agent.` }), command("edit", object({
|
|
41
|
+
command: constant("edit"),
|
|
42
|
+
name: argument(string({ metavar: "NAME" })),
|
|
43
|
+
}), { brief: message `Edit an agent.` }), command("delete", object({
|
|
44
|
+
command: constant("delete"),
|
|
45
|
+
name: argument(string({ metavar: "NAME" })),
|
|
46
|
+
}), { brief: message `Delete an agent.` }), command("secret", object({
|
|
47
|
+
command: constant("secret"),
|
|
48
|
+
subcommand: or(command("list", constant("list"), { brief: message `List secrets.` }), command("add", object({
|
|
49
|
+
action: constant("add"),
|
|
50
|
+
name: argument(string({ metavar: "NAME" })),
|
|
51
|
+
value: optional(argument(string({ metavar: "VALUE" }))),
|
|
52
|
+
}), { brief: message `Add a secret.` }), command("remove", object({
|
|
53
|
+
action: constant("remove"),
|
|
54
|
+
name: argument(string({ metavar: "NAME" })),
|
|
55
|
+
}), { brief: message `Remove a secret.` }), command("genenv", constant("genenv"), {
|
|
56
|
+
brief: message `Generate .env file.`,
|
|
57
|
+
})),
|
|
58
|
+
}), { brief: message `Secrets management.` }));
|
|
59
|
+
const meta = {
|
|
60
|
+
programName: "town",
|
|
61
|
+
help: "both",
|
|
62
|
+
version: "both",
|
|
63
|
+
completion: "both",
|
|
64
|
+
brief: message `Your one-stop shop for all things Town in the terminal\n`,
|
|
65
|
+
description: message `Town CLI is a first-class command-line experience for working with Town Agents.`,
|
|
66
|
+
};
|
|
67
|
+
async function main(parser, meta) {
|
|
68
|
+
const result = run(parser, meta);
|
|
69
|
+
await match(result)
|
|
70
|
+
// TODO
|
|
71
|
+
.with("deploy", async () => { })
|
|
72
|
+
.with({ command: "create" }, async ({ name, model, tools, systemPrompt }) => {
|
|
73
|
+
// Create command starts a long-running Ink session
|
|
74
|
+
// Only pass defined properties to satisfy exactOptionalPropertyTypes
|
|
75
|
+
await createCommand({
|
|
76
|
+
...(name !== undefined && { name }),
|
|
77
|
+
...(model !== undefined && { model }),
|
|
78
|
+
...(tools.length > 0 && { tools }),
|
|
79
|
+
...(systemPrompt !== undefined && { systemPrompt }),
|
|
80
|
+
});
|
|
81
|
+
})
|
|
82
|
+
.with("list", async () => {
|
|
83
|
+
await listCommand();
|
|
84
|
+
})
|
|
85
|
+
.with({ command: "run" }, async ({ name, http, gui, port }) => {
|
|
86
|
+
const options = {
|
|
87
|
+
name,
|
|
88
|
+
http: http === true,
|
|
89
|
+
gui: gui === true,
|
|
90
|
+
};
|
|
91
|
+
if (port !== null && port !== undefined) {
|
|
92
|
+
options.port = port;
|
|
93
|
+
}
|
|
94
|
+
await runCommand(options);
|
|
95
|
+
})
|
|
96
|
+
.with({ command: "edit" }, async ({ name }) => {
|
|
97
|
+
await editCommand(name);
|
|
98
|
+
})
|
|
99
|
+
.with({ command: "delete" }, async ({ name }) => {
|
|
100
|
+
await deleteCommand(name);
|
|
101
|
+
})
|
|
102
|
+
.with({ command: "secret" }, async ({ subcommand }) => {
|
|
103
|
+
await match(subcommand)
|
|
104
|
+
.with("list", async () => {
|
|
105
|
+
const secrets = await listSecrets();
|
|
106
|
+
for (const secret of secrets) {
|
|
107
|
+
console.log(`${secret.key}\t${secret.value}\t${secret.valid ? "✓" : "✗"}\t${secret.error ?? ""}`);
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
.with({ action: "add" }, async ({ name, value }) => {
|
|
111
|
+
// If value is not provided, prompt securely
|
|
112
|
+
const secretValue = value ?? (await promptSecret(name));
|
|
113
|
+
if (!secretValue) {
|
|
114
|
+
console.error("Error: Secret value cannot be empty");
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
await createSecret(name, secretValue);
|
|
118
|
+
console.log(`Secret '${name}' added successfully.`);
|
|
119
|
+
})
|
|
120
|
+
.with({ action: "remove" }, async ({ name }) => {
|
|
121
|
+
await deleteSecret(name);
|
|
122
|
+
console.log(`Secret '${name}' removed successfully.`);
|
|
123
|
+
})
|
|
124
|
+
.with("genenv", async () => {
|
|
125
|
+
await genenv();
|
|
126
|
+
console.log(".env file generated successfully.");
|
|
127
|
+
})
|
|
128
|
+
.exhaustive();
|
|
129
|
+
})
|
|
130
|
+
.exhaustive();
|
|
131
|
+
}
|
|
132
|
+
main(parser, meta).catch((error) => {
|
|
133
|
+
console.error("Error:", error);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@townco/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"townco": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/federicoweber/agent_hub.git"
|
|
17
|
+
},
|
|
18
|
+
"author": "Federico Weber",
|
|
19
|
+
"engines": {
|
|
20
|
+
"bun": ">=1.3.0"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"check": "tsc --noEmit",
|
|
24
|
+
"build": "tsc"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@townco/tsconfig": "^0.1.0",
|
|
28
|
+
"@types/bun": "^1.3.1",
|
|
29
|
+
"@types/react": "^19.2.2"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@optique/core": "^0.6.2",
|
|
33
|
+
"@optique/run": "^0.6.2",
|
|
34
|
+
"@townco/secret": "^0.1.0",
|
|
35
|
+
"@townco/ui": "^0.1.0",
|
|
36
|
+
"@townco/agent": "^0.1.0",
|
|
37
|
+
"@types/inquirer": "^9.0.9",
|
|
38
|
+
"ink": "^6.4.0",
|
|
39
|
+
"ink-text-input": "^6.0.0",
|
|
40
|
+
"inquirer": "^12.10.0",
|
|
41
|
+
"react": "^19.2.0",
|
|
42
|
+
"ts-pattern": "^5.9.0"
|
|
43
|
+
}
|
|
44
|
+
}
|