@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.
- package/dist/commands/batch.d.ts +8 -0
- package/dist/commands/batch.js +270 -0
- package/dist/commands/login.js +4 -4
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +286 -17
- package/dist/commands/whoami.js +5 -5
- package/dist/index.js +37 -2
- package/dist/lib/auth-fetch.d.ts +7 -5
- package/dist/lib/auth-fetch.js +35 -34
- package/package.json +8 -8
- package/dist/commands/delete.d.ts +0 -1
- package/dist/commands/delete.js +0 -60
- package/dist/commands/edit.d.ts +0 -1
- package/dist/commands/edit.js +0 -92
- package/dist/commands/list.d.ts +0 -1
- package/dist/commands/list.js +0 -55
- package/dist/commands/mcp-add.d.ts +0 -14
- package/dist/commands/mcp-add.js +0 -494
- package/dist/commands/mcp-list.d.ts +0 -3
- package/dist/commands/mcp-list.js +0 -63
- package/dist/commands/mcp-remove.d.ts +0 -3
- package/dist/commands/mcp-remove.js +0 -120
- package/dist/commands/tool-add.d.ts +0 -6
- package/dist/commands/tool-add.js +0 -349
- package/dist/commands/tool-list.d.ts +0 -3
- package/dist/commands/tool-list.js +0 -61
- package/dist/commands/tool-register.d.ts +0 -7
- package/dist/commands/tool-register.js +0 -291
- package/dist/commands/tool-remove.d.ts +0 -3
- package/dist/commands/tool-remove.js +0 -202
- package/dist/components/MergedLogsPane.d.ts +0 -11
- package/dist/components/MergedLogsPane.js +0 -205
- package/dist/lib/auth-storage.d.ts +0 -38
- package/dist/lib/auth-storage.js +0 -89
- package/dist/lib/mcp-storage.d.ts +0 -32
- package/dist/lib/mcp-storage.js +0 -111
|
@@ -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
|
+
}
|
package/dist/commands/login.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clearAuthCredentials,
|
|
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 =
|
|
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 ${
|
|
98
|
+
console.log(`Credentials saved to ${authFilePath}`);
|
|
99
99
|
}
|
|
100
100
|
catch (error) {
|
|
101
101
|
if (error instanceof TypeError && error.message.includes("fetch")) {
|