@townco/cli 0.1.73 → 0.1.74

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,8 @@
1
+ export interface BatchCommandOptions {
2
+ name: string;
3
+ queries?: string[];
4
+ file?: string;
5
+ concurrency?: number;
6
+ port?: number;
7
+ }
8
+ export declare function batchCommand(options: BatchCommandOptions): Promise<void>;
@@ -0,0 +1,270 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { isInsideTownProject } from "@townco/agent/storage";
5
+ import { createLogger } from "@townco/core";
6
+ import { AcpClient } from "@townco/ui";
7
+ import { findAvailablePort } from "../lib/port-utils.js";
8
+ // Helper to wait for server to be ready
9
+ async function waitForServer(url, maxRetries = 30) {
10
+ for (let i = 0; i < maxRetries; i++) {
11
+ try {
12
+ const response = await fetch(url);
13
+ if (response.ok) {
14
+ return true;
15
+ }
16
+ }
17
+ catch {
18
+ // Server not ready yet
19
+ }
20
+ await new Promise((resolve) => setTimeout(resolve, 500));
21
+ }
22
+ return false;
23
+ }
24
+ // Run a single query
25
+ async function runQuery(agentPort, query, queryNum, total, logger) {
26
+ logger.info("Starting query", { queryNum, total, query });
27
+ console.log(`[${queryNum}/${total}] Starting: "${query}"`);
28
+ const client = new AcpClient({
29
+ type: "http",
30
+ options: {
31
+ baseUrl: `http://localhost:${agentPort}`,
32
+ },
33
+ });
34
+ try {
35
+ // Connect and create a new session
36
+ await client.connect();
37
+ const sessionId = await client.startSession();
38
+ if (!sessionId) {
39
+ throw new Error("Failed to create session");
40
+ }
41
+ logger.info("Session created", { sessionId });
42
+ console.log(`[${queryNum}/${total}] Session: ${sessionId}`);
43
+ // Send the query
44
+ await client.sendMessage(query, sessionId);
45
+ // Collect the response (but don't print it - just wait for completion)
46
+ for await (const chunk of client.receiveMessages()) {
47
+ if (chunk.type === "content" && chunk.isComplete) {
48
+ break;
49
+ }
50
+ }
51
+ console.log(`[${queryNum}/${total}] ✓ Completed`);
52
+ logger.info("Query completed", { queryNum });
53
+ await client.disconnect();
54
+ }
55
+ catch (error) {
56
+ const errorMsg = error instanceof Error ? error.message : String(error);
57
+ console.error(`[${queryNum}/${total}] ✗ Error: ${errorMsg}`);
58
+ logger.error("Query failed", { queryNum, error: errorMsg });
59
+ await client.disconnect();
60
+ throw error;
61
+ }
62
+ }
63
+ // Run queries in batches with concurrency limit
64
+ async function runQueriesWithConcurrency(agentPort, queries, concurrency, logger) {
65
+ const total = queries.length;
66
+ let completed = 0;
67
+ let currentIndex = 0;
68
+ // Worker function
69
+ const worker = async () => {
70
+ while (currentIndex < queries.length) {
71
+ const index = currentIndex++;
72
+ const query = queries[index];
73
+ if (!query)
74
+ continue; // Skip if undefined (shouldn't happen)
75
+ const queryNum = index + 1;
76
+ try {
77
+ await runQuery(agentPort, query, queryNum, total, logger);
78
+ completed++;
79
+ }
80
+ catch (error) {
81
+ console.error(`Query ${queryNum} failed:`, error);
82
+ completed++;
83
+ }
84
+ }
85
+ };
86
+ // Start workers
87
+ const workers = Array.from({ length: concurrency }, () => worker());
88
+ await Promise.all(workers);
89
+ console.log(`\n✓ All ${completed}/${total} queries completed\n`);
90
+ logger.info("All queries completed", { total, completed });
91
+ }
92
+ export async function batchCommand(options) {
93
+ const { name, queries: inlineQueries = [], file, concurrency = 10, port = 3100, } = options;
94
+ const logger = createLogger("batch-command", "debug");
95
+ // Check if we're inside a Town project
96
+ const projectRoot = await isInsideTownProject();
97
+ if (projectRoot === null) {
98
+ console.error("Error: Not inside a Town project.");
99
+ console.log("\nPlease run this command inside a project directory, or run:\n" +
100
+ " town create --init <path>\n" +
101
+ "to create a project.");
102
+ process.exit(1);
103
+ }
104
+ // Load queries from file or use inline queries
105
+ let queries = [];
106
+ if (file !== undefined) {
107
+ const filePath = file; // Type narrowing for TypeScript
108
+ try {
109
+ const content = await readFile(filePath, "utf-8");
110
+ // Split by newlines, trim, and filter empty lines
111
+ queries = content
112
+ .split("\n")
113
+ .map((line) => line.trim())
114
+ .filter((line) => line.length > 0 && !line.startsWith("#"));
115
+ if (queries.length === 0) {
116
+ console.error(`Error: No queries found in file: ${filePath}`);
117
+ process.exit(1);
118
+ }
119
+ console.log(`Loaded ${queries.length} queries from ${filePath}\n`);
120
+ }
121
+ catch (error) {
122
+ console.error(`Error reading file: ${filePath}`);
123
+ console.error(error instanceof Error ? error.message : String(error));
124
+ process.exit(1);
125
+ }
126
+ }
127
+ else if (inlineQueries.length > 0) {
128
+ queries = inlineQueries;
129
+ }
130
+ else {
131
+ console.error("Error: No queries provided.");
132
+ console.log("\nUsage:");
133
+ console.log(' town batch <agent> "query1" "query2" ...');
134
+ console.log(" town batch <agent> --file queries.txt");
135
+ process.exit(1);
136
+ }
137
+ console.log(`=== Batch Query Execution ===`);
138
+ console.log(`Agent: ${name}`);
139
+ console.log(`Queries: ${queries.length}`);
140
+ console.log(`Concurrency: ${concurrency}\n`);
141
+ const agentPath = join(projectRoot, "agents", name);
142
+ const binPath = join(agentPath, "bin.ts");
143
+ const tracesDbPath = join(agentPath, ".traces.db");
144
+ // Check if agent exists
145
+ try {
146
+ const { stat } = await import("node:fs/promises");
147
+ await stat(agentPath);
148
+ }
149
+ catch {
150
+ console.error(`Error: Agent "${name}" not found.`);
151
+ console.log('\nCreate an agent with "town create" or list agents with "town list"');
152
+ process.exit(1);
153
+ }
154
+ // Load agent display name for debugger
155
+ let agentDisplayName = name;
156
+ try {
157
+ const agentIndexPath = join(agentPath, "index.ts");
158
+ const content = await readFile(agentIndexPath, "utf-8");
159
+ const displayNameMatch = content.match(/displayName:\s*["']([^"']+)["']/);
160
+ if (displayNameMatch?.[1]) {
161
+ agentDisplayName = displayNameMatch[1];
162
+ }
163
+ }
164
+ catch {
165
+ // Fallback to directory name
166
+ }
167
+ let debuggerProcess = null;
168
+ let agentProcess = null;
169
+ // Cleanup function
170
+ const cleanup = () => {
171
+ logger.info("Cleaning up processes");
172
+ if (agentProcess?.pid) {
173
+ try {
174
+ process.kill(-agentProcess.pid, "SIGINT");
175
+ }
176
+ catch (e) {
177
+ // Process may already be dead
178
+ }
179
+ }
180
+ if (debuggerProcess?.pid) {
181
+ try {
182
+ process.kill(-debuggerProcess.pid, "SIGKILL");
183
+ }
184
+ catch (e) {
185
+ // Process may already be dead
186
+ }
187
+ }
188
+ };
189
+ // Register cleanup handlers
190
+ process.on("SIGINT", () => {
191
+ cleanup();
192
+ process.exit(0);
193
+ });
194
+ process.on("SIGTERM", () => {
195
+ cleanup();
196
+ process.exit(0);
197
+ });
198
+ try {
199
+ // 1. Start debugger process
200
+ console.log("Starting debugger...");
201
+ const debuggerPkgPath = require.resolve("@townco/debugger/package.json");
202
+ const debuggerDir = dirname(debuggerPkgPath);
203
+ debuggerProcess = spawn("bun", ["src/index.ts"], {
204
+ cwd: debuggerDir,
205
+ stdio: "ignore",
206
+ detached: true,
207
+ env: {
208
+ ...process.env,
209
+ DB_PATH: tracesDbPath,
210
+ AGENT_NAME: agentDisplayName,
211
+ },
212
+ });
213
+ // Wait for debugger to be ready
214
+ const debuggerReady = await waitForServer("http://localhost:4318/health");
215
+ if (!debuggerReady) {
216
+ throw new Error("Debugger failed to start");
217
+ }
218
+ console.log("✓ Debugger ready (UI: http://localhost:4000)\n");
219
+ logger.info("Debugger started");
220
+ // 2. Start agent HTTP server
221
+ console.log("Starting agent HTTP server...");
222
+ // Find available port
223
+ const availablePort = await findAvailablePort(port);
224
+ if (availablePort !== port) {
225
+ console.log(`Port ${port} in use, using port ${availablePort} instead`);
226
+ }
227
+ agentProcess = spawn("bun", [binPath, "http"], {
228
+ cwd: agentPath,
229
+ stdio: "ignore",
230
+ detached: true,
231
+ env: {
232
+ ...process.env,
233
+ PORT: String(availablePort),
234
+ ENABLE_TELEMETRY: "true",
235
+ },
236
+ });
237
+ // Wait for agent to be ready
238
+ const agentReady = await waitForServer(`http://localhost:${availablePort}/health`);
239
+ if (!agentReady) {
240
+ throw new Error("Agent failed to start");
241
+ }
242
+ console.log(`✓ Agent ready (port ${availablePort})\n`);
243
+ logger.info("Agent started", { port: availablePort });
244
+ // 3. Run queries in parallel
245
+ console.log(`Running ${queries.length} queries...\n`);
246
+ await runQueriesWithConcurrency(availablePort, queries, concurrency, logger);
247
+ // 4. Give telemetry time to flush
248
+ console.log("Flushing telemetry data...");
249
+ await new Promise((resolve) => setTimeout(resolve, 3000));
250
+ console.log("========================================");
251
+ console.log(`✓ All ${queries.length} queries completed!`);
252
+ console.log("");
253
+ console.log("Results saved to:");
254
+ console.log(` Sessions: agents/${name}/.sessions/`);
255
+ console.log(` Traces: agents/${name}/.traces.db`);
256
+ console.log("");
257
+ console.log("View traces at: http://localhost:4000");
258
+ console.log("========================================");
259
+ }
260
+ catch (error) {
261
+ console.error("\nError:", error instanceof Error ? error.message : String(error));
262
+ logger.error("Batch command failed", {
263
+ error: error instanceof Error ? error.message : String(error),
264
+ });
265
+ process.exit(1);
266
+ }
267
+ finally {
268
+ cleanup();
269
+ }
270
+ }
@@ -1,4 +1,4 @@
1
- import { clearAuthCredentials, getAuthFilePath, getShedUrl, loadAuthCredentials, saveAuthCredentials, } from "@townco/core/auth";
1
+ import { clearAuthCredentials, getShedUrl, loadAuthCredentialsFromDisk, saveAuthCredentials, } from "@townco/core/auth";
2
2
  import inquirer from "inquirer";
3
3
  // ============================================================================
4
4
  // Command Implementation
@@ -7,7 +7,7 @@ export async function loginCommand() {
7
7
  const shedUrl = getShedUrl();
8
8
  console.log("Town Login\n");
9
9
  // Check if already logged in
10
- const existingCredentials = loadAuthCredentials();
10
+ const existingCredentials = loadAuthCredentialsFromDisk();
11
11
  if (existingCredentials) {
12
12
  console.log(`Currently logged in as: ${existingCredentials.user.email}`);
13
13
  console.log(`Shed URL: ${existingCredentials.shed_url}\n`);
@@ -93,9 +93,9 @@ export async function loginCommand() {
93
93
  user: data.user,
94
94
  shed_url: shedUrl,
95
95
  };
96
- saveAuthCredentials(credentials);
96
+ const authFilePath = saveAuthCredentials(credentials);
97
97
  console.log(`\nLogged in as ${data.user.email}`);
98
- console.log(`Credentials saved to ${getAuthFilePath()}`);
98
+ console.log(`Credentials saved to ${authFilePath}`);
99
99
  }
100
100
  catch (error) {
101
101
  if (error instanceof TypeError && error.message.includes("fetch")) {
@@ -2,6 +2,8 @@ export interface RunCommandOptions {
2
2
  name: string;
3
3
  http?: boolean;
4
4
  gui?: boolean;
5
+ cli?: boolean;
6
+ prompt?: string;
5
7
  port?: number;
6
8
  noSession?: boolean;
7
9
  }