@townco/cli 0.1.72 → 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 +288 -19
- package/dist/commands/whoami.js +5 -5
- package/dist/index.js +40 -11
- package/dist/lib/auth-fetch.d.ts +7 -5
- package/dist/lib/auth-fetch.js +35 -34
- package/package.json +8 -8
|
@@ -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")) {
|
package/dist/commands/run.d.ts
CHANGED
package/dist/commands/run.js
CHANGED
|
@@ -146,8 +146,235 @@ async function loadEnvVars(projectRoot, logger) {
|
|
|
146
146
|
}
|
|
147
147
|
return envVars;
|
|
148
148
|
}
|
|
149
|
+
// CLI mode runner - outputs directly to stdout without React UI
|
|
150
|
+
async function runCliMode(options) {
|
|
151
|
+
const { binPath, workingDir, prompt, configEnvVars, noSession, waitForDebugger = false, } = options;
|
|
152
|
+
// Wait for debugger to be ready BEFORE starting agent
|
|
153
|
+
if (waitForDebugger) {
|
|
154
|
+
let debuggerReady = false;
|
|
155
|
+
let retries = 30; // 3 seconds
|
|
156
|
+
while (!debuggerReady && retries > 0) {
|
|
157
|
+
try {
|
|
158
|
+
const response = await fetch(`http://localhost:4318/v1/traces`, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: { "Content-Type": "application/json" },
|
|
161
|
+
body: JSON.stringify({}),
|
|
162
|
+
});
|
|
163
|
+
// Even a 405 or 400 means the server is up and responding
|
|
164
|
+
debuggerReady = true;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
// Server not ready yet
|
|
169
|
+
}
|
|
170
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
171
|
+
retries--;
|
|
172
|
+
}
|
|
173
|
+
if (!debuggerReady) {
|
|
174
|
+
process.stderr.write("Warning: Debugger did not start in time, traces may not be collected\n");
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Start HTTP server for the agent (silently in CLI mode)
|
|
178
|
+
// Find available port
|
|
179
|
+
const { findAvailablePort } = await import("../lib/port-utils.js");
|
|
180
|
+
const agentPort = await findAvailablePort(3100);
|
|
181
|
+
// Spawn agent process with HTTP mode
|
|
182
|
+
const agentProcess = spawn("bun", [binPath, "http"], {
|
|
183
|
+
cwd: workingDir,
|
|
184
|
+
stdio: ["ignore", "inherit", "inherit"], // Show agent output for debugging
|
|
185
|
+
detached: true,
|
|
186
|
+
env: {
|
|
187
|
+
...process.env,
|
|
188
|
+
...configEnvVars,
|
|
189
|
+
PORT: String(agentPort),
|
|
190
|
+
ENABLE_TELEMETRY: "true",
|
|
191
|
+
...(noSession ? { TOWN_NO_SESSION: "true" } : {}),
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
// Log any agent errors
|
|
195
|
+
agentProcess.on("error", (err) => {
|
|
196
|
+
process.stderr.write(`Agent process error: ${err.message}\n`);
|
|
197
|
+
});
|
|
198
|
+
agentProcess.on("exit", () => {
|
|
199
|
+
// Agent exit is expected, no need to log in CLI mode
|
|
200
|
+
});
|
|
201
|
+
// Wait for server to be ready
|
|
202
|
+
let serverReady = false;
|
|
203
|
+
let retries = 30; // 15 seconds
|
|
204
|
+
while (!serverReady && retries > 0) {
|
|
205
|
+
try {
|
|
206
|
+
const response = await fetch(`http://localhost:${agentPort}/health`);
|
|
207
|
+
if (response.ok) {
|
|
208
|
+
serverReady = true;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// Server not ready yet
|
|
214
|
+
}
|
|
215
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
216
|
+
retries--;
|
|
217
|
+
}
|
|
218
|
+
if (!serverReady) {
|
|
219
|
+
process.stderr.write("Agent failed to start\n");
|
|
220
|
+
agentProcess.kill();
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
// Create ACP client with HTTP transport
|
|
224
|
+
const client = new AcpClient({
|
|
225
|
+
type: "http",
|
|
226
|
+
options: {
|
|
227
|
+
baseUrl: `http://localhost:${agentPort}`,
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
// Connect the client
|
|
231
|
+
await client.connect();
|
|
232
|
+
try {
|
|
233
|
+
// Start session
|
|
234
|
+
const sessionId = await client.startSession();
|
|
235
|
+
if (!sessionId) {
|
|
236
|
+
process.stderr.write("Failed to create session\n");
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
// Echo the user prompt
|
|
240
|
+
console.log("\n> User:", prompt);
|
|
241
|
+
console.log();
|
|
242
|
+
// Send message (HTTP transport works properly)
|
|
243
|
+
await client.sendMessage(prompt, sessionId);
|
|
244
|
+
// Track tool calls and message state
|
|
245
|
+
const toolCalls = new Map();
|
|
246
|
+
let currentAssistantMessage = "";
|
|
247
|
+
let isStreamingText = false;
|
|
248
|
+
// Receive and render messages
|
|
249
|
+
for await (const chunk of client.receiveMessages()) {
|
|
250
|
+
if (chunk.type === "content") {
|
|
251
|
+
// Content chunk - text streaming
|
|
252
|
+
if (chunk.contentDelta?.type === "text") {
|
|
253
|
+
// Stream assistant text response
|
|
254
|
+
if (!isStreamingText) {
|
|
255
|
+
process.stdout.write("⏺ Assistant: ");
|
|
256
|
+
isStreamingText = true;
|
|
257
|
+
}
|
|
258
|
+
process.stdout.write(chunk.contentDelta.text);
|
|
259
|
+
currentAssistantMessage += chunk.contentDelta.text;
|
|
260
|
+
}
|
|
261
|
+
// Check if complete after processing content
|
|
262
|
+
if (chunk.isComplete) {
|
|
263
|
+
if (isStreamingText) {
|
|
264
|
+
console.log("\n");
|
|
265
|
+
isStreamingText = false;
|
|
266
|
+
}
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
else if (chunk.type === "tool_call") {
|
|
271
|
+
// New tool call
|
|
272
|
+
if (isStreamingText) {
|
|
273
|
+
console.log("\n");
|
|
274
|
+
isStreamingText = false;
|
|
275
|
+
}
|
|
276
|
+
const toolCall = chunk.toolCall;
|
|
277
|
+
toolCalls.set(toolCall.id, {
|
|
278
|
+
id: toolCall.id,
|
|
279
|
+
name: toolCall.title || toolCall.name,
|
|
280
|
+
prettyName: toolCall.prettyName,
|
|
281
|
+
status: toolCall.status || "pending",
|
|
282
|
+
input: toolCall.rawInput,
|
|
283
|
+
});
|
|
284
|
+
const prettyName = toolCall.prettyName || toolCall.title || toolCall.name;
|
|
285
|
+
console.log(`\n🔧 Tool: ${prettyName}`);
|
|
286
|
+
console.log(` Status: ${toolCall.status || "pending"}`);
|
|
287
|
+
if (toolCall.rawInput && Object.keys(toolCall.rawInput).length > 0) {
|
|
288
|
+
console.log(` Input: ${JSON.stringify(toolCall.rawInput, null, 2).split("\n").join("\n ")}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else if (chunk.type === "tool_call_update") {
|
|
292
|
+
// Update existing tool call
|
|
293
|
+
const toolCallUpdate = chunk.toolCallUpdate;
|
|
294
|
+
const toolCall = toolCalls.get(toolCallUpdate.id);
|
|
295
|
+
if (toolCall) {
|
|
296
|
+
if (toolCallUpdate.status) {
|
|
297
|
+
toolCall.status = toolCallUpdate.status;
|
|
298
|
+
}
|
|
299
|
+
if (toolCallUpdate.rawInput) {
|
|
300
|
+
toolCall.input = toolCallUpdate.rawInput;
|
|
301
|
+
}
|
|
302
|
+
// Print status update
|
|
303
|
+
const prettyName = toolCall.prettyName || toolCall.name;
|
|
304
|
+
if (toolCallUpdate.status) {
|
|
305
|
+
console.log(`\n🔧 Tool: ${prettyName}`);
|
|
306
|
+
console.log(` Status: ${toolCallUpdate.status}`);
|
|
307
|
+
}
|
|
308
|
+
// Handle tool output
|
|
309
|
+
if (toolCallUpdate.content && Array.isArray(toolCallUpdate.content)) {
|
|
310
|
+
let outputText = "";
|
|
311
|
+
for (const block of toolCallUpdate.content) {
|
|
312
|
+
if (block.type === "content" && block.content.type === "text") {
|
|
313
|
+
outputText += block.content.text;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (outputText) {
|
|
317
|
+
toolCall.output = outputText;
|
|
318
|
+
// Truncate very long output
|
|
319
|
+
const maxOutputLength = 500;
|
|
320
|
+
if (outputText.length > maxOutputLength) {
|
|
321
|
+
const truncated = outputText.substring(0, maxOutputLength);
|
|
322
|
+
console.log(` Output: ${truncated}...`);
|
|
323
|
+
console.log(` (${outputText.length} characters, truncated)`);
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
console.log(` Output: ${outputText}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Final newline
|
|
334
|
+
if (isStreamingText) {
|
|
335
|
+
console.log("\n");
|
|
336
|
+
}
|
|
337
|
+
// Disconnect client and cleanup
|
|
338
|
+
await client.disconnect();
|
|
339
|
+
// Gracefully shutdown agent process with SIGINT (triggers telemetry flush)
|
|
340
|
+
if (agentProcess.pid) {
|
|
341
|
+
try {
|
|
342
|
+
process.kill(-agentProcess.pid, "SIGINT");
|
|
343
|
+
// Wait for agent to fully exit (including telemetry flush)
|
|
344
|
+
await new Promise((resolve) => {
|
|
345
|
+
agentProcess.on("exit", () => resolve());
|
|
346
|
+
// Fallback timeout in case agent doesn't exit
|
|
347
|
+
setTimeout(() => resolve(), 3000);
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
catch (e) {
|
|
351
|
+
// Process may already be dead
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
console.error("\nError:", error instanceof Error ? error.message : String(error));
|
|
357
|
+
console.error("Stack:", error instanceof Error ? error.stack : "");
|
|
358
|
+
try {
|
|
359
|
+
await client.disconnect();
|
|
360
|
+
}
|
|
361
|
+
catch (disconnectError) {
|
|
362
|
+
console.error("Failed to disconnect:", disconnectError);
|
|
363
|
+
}
|
|
364
|
+
// Kill agent process on error
|
|
365
|
+
if (agentProcess.pid) {
|
|
366
|
+
try {
|
|
367
|
+
process.kill(-agentProcess.pid, "SIGKILL");
|
|
368
|
+
}
|
|
369
|
+
catch (e) {
|
|
370
|
+
// Process may already be dead
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
149
376
|
export async function runCommand(options) {
|
|
150
|
-
const { name, http = false, gui = false, port = 3100, noSession = false, } = options;
|
|
377
|
+
const { name, http = false, gui = false, cli = false, prompt, port = 3100, noSession = false, } = options;
|
|
151
378
|
// Check if we're inside a Town project
|
|
152
379
|
const projectRoot = await isInsideTownProject();
|
|
153
380
|
if (projectRoot === null) {
|
|
@@ -161,43 +388,51 @@ export async function runCommand(options) {
|
|
|
161
388
|
const configEnvVars = await loadEnvVars(projectRoot);
|
|
162
389
|
// Resolve agent path to load agent definition
|
|
163
390
|
const agentPath = join(projectRoot, "agents", name);
|
|
164
|
-
// Load agent definition to get displayName by parsing the file
|
|
391
|
+
// Load agent definition to get displayName and detect MCPs by parsing the file
|
|
165
392
|
let agentDisplayName = name; // Fallback to agent directory name
|
|
393
|
+
let usesBibliotechaMcp = false;
|
|
166
394
|
try {
|
|
167
395
|
const agentIndexPath = join(agentPath, "index.ts");
|
|
168
396
|
const content = await readFile(agentIndexPath, "utf-8");
|
|
169
397
|
// Match displayName in the agent definition object
|
|
170
398
|
// Looking for patterns like: displayName: "Researcher" or displayName: 'Researcher'
|
|
171
|
-
const
|
|
172
|
-
if (
|
|
173
|
-
agentDisplayName =
|
|
399
|
+
const displayNameMatch = content.match(/displayName:\s*["']([^"']+)["']/);
|
|
400
|
+
if (displayNameMatch?.[1]) {
|
|
401
|
+
agentDisplayName = displayNameMatch[1];
|
|
174
402
|
}
|
|
403
|
+
// Detect if agent uses bibliotecha MCP
|
|
404
|
+
// Looking for patterns like: name: "bibliotecha" or name: 'bibliotecha'
|
|
405
|
+
usesBibliotechaMcp = /name:\s*["']bibliotecha["']/.test(content);
|
|
175
406
|
}
|
|
176
407
|
catch (error) {
|
|
177
408
|
// If we can't read the agent definition, just use the directory name
|
|
178
409
|
// Silently fail - the directory name is a reasonable fallback
|
|
179
410
|
}
|
|
180
411
|
// Start the debugger server as subprocess (OTLP collector + UI)
|
|
412
|
+
let debuggerProcess = null;
|
|
413
|
+
let cleanupDebugger = null;
|
|
181
414
|
const debuggerPkgPath = require.resolve("@townco/debugger/package.json");
|
|
182
415
|
const debuggerDir = dirname(debuggerPkgPath);
|
|
183
|
-
|
|
416
|
+
debuggerProcess = spawn("bun", ["src/index.ts"], {
|
|
184
417
|
cwd: debuggerDir,
|
|
185
|
-
stdio: "inherit",
|
|
418
|
+
stdio: cli ? "ignore" : "inherit", // Silent in CLI mode
|
|
186
419
|
detached: true,
|
|
187
420
|
env: {
|
|
188
421
|
...process.env,
|
|
189
|
-
DB_PATH: join(projectRoot, ".traces.db"),
|
|
422
|
+
DB_PATH: join(projectRoot, "agents", name, ".traces.db"),
|
|
190
423
|
AGENT_NAME: agentDisplayName,
|
|
191
424
|
},
|
|
192
425
|
});
|
|
193
|
-
|
|
426
|
+
if (!cli) {
|
|
427
|
+
console.log(`Debugger UI: http://localhost:4000`);
|
|
428
|
+
}
|
|
194
429
|
// Cleanup debugger process on exit
|
|
195
430
|
let isDebuggerCleaningUp = false;
|
|
196
|
-
|
|
431
|
+
cleanupDebugger = () => {
|
|
197
432
|
if (isDebuggerCleaningUp)
|
|
198
433
|
return;
|
|
199
434
|
isDebuggerCleaningUp = true;
|
|
200
|
-
if (debuggerProcess
|
|
435
|
+
if (debuggerProcess?.pid) {
|
|
201
436
|
try {
|
|
202
437
|
process.kill(-debuggerProcess.pid, "SIGKILL");
|
|
203
438
|
}
|
|
@@ -206,7 +441,11 @@ export async function runCommand(options) {
|
|
|
206
441
|
}
|
|
207
442
|
}
|
|
208
443
|
};
|
|
209
|
-
|
|
444
|
+
// Only register exit handler for non-CLI modes
|
|
445
|
+
// In CLI mode, we cleanup manually after agent exits
|
|
446
|
+
if (!cli) {
|
|
447
|
+
process.on("exit", cleanupDebugger);
|
|
448
|
+
}
|
|
210
449
|
// Check if agent exists
|
|
211
450
|
try {
|
|
212
451
|
const { stat } = await import("node:fs/promises");
|
|
@@ -222,7 +461,7 @@ export async function runCommand(options) {
|
|
|
222
461
|
const logger = createLogger("cli", "debug");
|
|
223
462
|
logger.info("Starting agent", {
|
|
224
463
|
name,
|
|
225
|
-
mode: gui ? "gui" : http ? "http" : "tui",
|
|
464
|
+
mode: gui ? "gui" : http ? "http" : cli ? "cli" : "tui",
|
|
226
465
|
agentPath,
|
|
227
466
|
});
|
|
228
467
|
// If GUI mode, run with tabbed interface
|
|
@@ -275,15 +514,21 @@ export async function runCommand(options) {
|
|
|
275
514
|
},
|
|
276
515
|
});
|
|
277
516
|
// Start the GUI dev server (no package.json, run vite directly) with detached process group
|
|
278
|
-
const guiProcess = spawn("bunx", ["vite"
|
|
517
|
+
const guiProcess = spawn("bunx", ["vite"], {
|
|
279
518
|
cwd: guiPath,
|
|
280
519
|
stdio: ["ignore", "pipe", "pipe"], // Pipe stdout/stderr for capture
|
|
281
520
|
detached: true,
|
|
282
521
|
env: {
|
|
283
522
|
...process.env,
|
|
284
523
|
...configEnvVars,
|
|
285
|
-
VITE_AGENT_URL: `http
|
|
524
|
+
VITE_AGENT_URL: `http://localhost:${availablePort}`,
|
|
286
525
|
VITE_DEBUGGER_URL: "http://localhost:4000",
|
|
526
|
+
// If agent uses bibliotecha MCP, pass BIBLIOTECHA_URL to GUI for auth
|
|
527
|
+
...(usesBibliotechaMcp &&
|
|
528
|
+
process.env.BIBLIOTECHA_URL && {
|
|
529
|
+
VITE_BIBLIOTECHA_URL: process.env.BIBLIOTECHA_URL,
|
|
530
|
+
VITE_BIBLIOTECHA_FRONTEND_URL: process.env.BIBLIOTECHA_FRONTEND_URL,
|
|
531
|
+
}),
|
|
287
532
|
},
|
|
288
533
|
});
|
|
289
534
|
// Setup cleanup handlers for agent and GUI processes
|
|
@@ -310,7 +555,7 @@ export async function runCommand(options) {
|
|
|
310
555
|
}
|
|
311
556
|
}
|
|
312
557
|
// Also cleanup debugger
|
|
313
|
-
cleanupDebugger();
|
|
558
|
+
cleanupDebugger?.();
|
|
314
559
|
};
|
|
315
560
|
process.on("exit", cleanupProcesses);
|
|
316
561
|
// Handle SIGINT (Control-C) explicitly and exit
|
|
@@ -372,7 +617,7 @@ export async function runCommand(options) {
|
|
|
372
617
|
}
|
|
373
618
|
}
|
|
374
619
|
// Also cleanup debugger
|
|
375
|
-
cleanupDebugger();
|
|
620
|
+
cleanupDebugger?.();
|
|
376
621
|
};
|
|
377
622
|
process.on("exit", cleanupAgentProcess);
|
|
378
623
|
// Handle SIGINT (Control-C) explicitly and exit
|
|
@@ -396,6 +641,30 @@ export async function runCommand(options) {
|
|
|
396
641
|
});
|
|
397
642
|
return;
|
|
398
643
|
}
|
|
644
|
+
// CLI mode: Run with single prompt and output to stdout
|
|
645
|
+
if (cli) {
|
|
646
|
+
if (!prompt) {
|
|
647
|
+
console.error("Error: --cli mode requires a PROMPT argument");
|
|
648
|
+
console.log('\nUsage: town run <agent> --cli "your prompt here"');
|
|
649
|
+
process.exit(1);
|
|
650
|
+
}
|
|
651
|
+
logger.info("Starting CLI mode", { name, agentPath, prompt });
|
|
652
|
+
// Run CLI mode and wait for completion
|
|
653
|
+
await runCliMode({
|
|
654
|
+
binPath,
|
|
655
|
+
agentPath,
|
|
656
|
+
workingDir: agentPath,
|
|
657
|
+
prompt,
|
|
658
|
+
configEnvVars,
|
|
659
|
+
noSession,
|
|
660
|
+
waitForDebugger: true, // Wait for debugger to be ready before starting agent
|
|
661
|
+
});
|
|
662
|
+
// Agent has exited cleanly, now cleanup debugger
|
|
663
|
+
if (cleanupDebugger) {
|
|
664
|
+
cleanupDebugger();
|
|
665
|
+
}
|
|
666
|
+
process.exit(0);
|
|
667
|
+
}
|
|
399
668
|
// Default: Start TUI interface with the agent
|
|
400
669
|
logger.info("Starting TUI mode", { name, agentPath });
|
|
401
670
|
// Clear terminal and move cursor to top
|
|
@@ -408,7 +677,7 @@ export async function runCommand(options) {
|
|
|
408
677
|
}
|
|
409
678
|
// Setup signal handlers for TUI mode
|
|
410
679
|
const handleTuiSigint = () => {
|
|
411
|
-
cleanupDebugger();
|
|
680
|
+
cleanupDebugger?.();
|
|
412
681
|
process.exit(0);
|
|
413
682
|
};
|
|
414
683
|
process.on("SIGINT", handleTuiSigint);
|
|
@@ -416,7 +685,7 @@ export async function runCommand(options) {
|
|
|
416
685
|
// Render the tabbed UI with Chat and Logs
|
|
417
686
|
const { waitUntilExit } = render(_jsx(TuiRunner, { agentPath: binPath, workingDir: agentPath, noSession: noSession, onExit: () => {
|
|
418
687
|
// Cleanup is handled by the ACP client disconnect
|
|
419
|
-
cleanupDebugger();
|
|
688
|
+
cleanupDebugger?.();
|
|
420
689
|
} }));
|
|
421
690
|
await waitUntilExit();
|
|
422
691
|
process.exit(0);
|
package/dist/commands/whoami.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getShedAuth } from "@townco/core/auth";
|
|
2
2
|
import { authGet } from "../lib/auth-fetch.js";
|
|
3
3
|
export async function whoamiCommand() {
|
|
4
|
-
const credentials =
|
|
4
|
+
const credentials = getShedAuth();
|
|
5
5
|
if (!credentials) {
|
|
6
|
-
console.log("Not logged in. Run 'town login' to authenticate.");
|
|
6
|
+
console.log("Not logged in. Run 'town login' or set SHED_API_KEY to authenticate.");
|
|
7
7
|
return;
|
|
8
8
|
}
|
|
9
|
-
console.log(`Shed URL: ${credentials.
|
|
9
|
+
console.log(`Shed URL: ${credentials.shedUrl}`);
|
|
10
10
|
console.log("Verifying credentials...\n");
|
|
11
11
|
try {
|
|
12
12
|
const response = await authGet("/api/cli/me");
|
|
@@ -25,7 +25,7 @@ export async function whoamiCommand() {
|
|
|
25
25
|
}
|
|
26
26
|
catch (error) {
|
|
27
27
|
if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
28
|
-
console.error(`Could not connect to ${credentials.
|
|
28
|
+
console.error(`Could not connect to ${credentials.shedUrl}`);
|
|
29
29
|
console.error("Please check your internet connection and try again.");
|
|
30
30
|
}
|
|
31
31
|
else {
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import walk from "ignore-walk";
|
|
|
17
17
|
import inquirer from "inquirer";
|
|
18
18
|
import superjson from "superjson";
|
|
19
19
|
import { match } from "ts-pattern";
|
|
20
|
+
import { batchCommand } from "./commands/batch.js";
|
|
20
21
|
import { configureCommand } from "./commands/configure.js";
|
|
21
22
|
import { createCommand } from "./commands/create.js";
|
|
22
23
|
import { createProjectCommand } from "./commands/create-project.js";
|
|
@@ -64,9 +65,21 @@ const parser = or(command("deploy", object({ command: constant("deploy") }), {
|
|
|
64
65
|
name: argument(string({ metavar: "NAME" })),
|
|
65
66
|
http: optional(flag("--http")),
|
|
66
67
|
gui: optional(flag("--gui")),
|
|
68
|
+
cli: optional(flag("--cli")),
|
|
69
|
+
prompt: optional(argument(string({ metavar: "PROMPT" }))),
|
|
67
70
|
port: optional(option("-p", "--port", integer())),
|
|
68
71
|
noSession: optional(flag("--no-session")),
|
|
69
|
-
}), { brief: message `Run an agent.` }), command("
|
|
72
|
+
}), { brief: message `Run an agent.` }), command("batch", object({
|
|
73
|
+
command: constant("batch"),
|
|
74
|
+
name: argument(string({ metavar: "NAME" })),
|
|
75
|
+
queries: multiple(argument(string({ metavar: "QUERY" }))),
|
|
76
|
+
file: optional(option("-f", "--file", string())),
|
|
77
|
+
concurrency: optional(option("-c", "--concurrency", integer())),
|
|
78
|
+
port: optional(option("-p", "--port", integer())),
|
|
79
|
+
}), {
|
|
80
|
+
brief: message `Run multiple queries in parallel against an agent.`,
|
|
81
|
+
description: message `Run multiple queries in parallel, each in its own session. Queries can be provided as arguments or loaded from a file.`,
|
|
82
|
+
}), command("secret", object({
|
|
70
83
|
command: constant("secret"),
|
|
71
84
|
subcommand: or(command("list", object({ action: constant("list") }), {
|
|
72
85
|
brief: message `List secrets.`,
|
|
@@ -122,24 +135,18 @@ const main = async (parser, meta) => await match(run(parser, meta))
|
|
|
122
135
|
],
|
|
123
136
|
});
|
|
124
137
|
console.log("Creating archive...");
|
|
125
|
-
const root = await findRoot(
|
|
138
|
+
const root = await findRoot();
|
|
126
139
|
const arc = archiver("tar", { gzip: true });
|
|
127
|
-
const chunks = [];
|
|
128
|
-
const done = new Promise((ok, err) => {
|
|
129
|
-
arc.on("data", (chunk) => chunks.push(chunk));
|
|
130
|
-
arc.on("end", () => ok(Buffer.concat(chunks)));
|
|
131
|
-
arc.on("error", err);
|
|
132
|
-
});
|
|
133
140
|
(await walk({ path: root, ignoreFiles: [".gitignore"] }))
|
|
134
141
|
.filter((path) => path.split("/")[0] !== ".git")
|
|
135
142
|
.forEach((path) => {
|
|
136
143
|
if (!fs.statSync(path).isFile())
|
|
137
144
|
return;
|
|
138
|
-
arc.append(
|
|
145
|
+
arc.append(path, { name: path });
|
|
139
146
|
});
|
|
140
147
|
arc.finalize();
|
|
141
148
|
console.log("Uploading archive...");
|
|
142
|
-
const { sha256, cached } = await client.uploadArchive.mutate(await
|
|
149
|
+
const { sha256, cached } = await client.uploadArchive.mutate(Buffer.from(await Array.fromAsync(arc)));
|
|
143
150
|
console.log(`Archive uploaded: ${sha256} (${cached ? "cached" : "new"})`);
|
|
144
151
|
console.log("Deploying...");
|
|
145
152
|
client.deploy.subscribe({ sha256 }, {
|
|
@@ -221,17 +228,39 @@ const main = async (parser, meta) => await match(run(parser, meta))
|
|
|
221
228
|
}
|
|
222
229
|
}
|
|
223
230
|
})
|
|
224
|
-
.with({ command: "run" }, async ({ name, http, gui, port, noSession }) => {
|
|
231
|
+
.with({ command: "run" }, async ({ name, http, gui, cli, prompt, port, noSession }) => {
|
|
225
232
|
const options = {
|
|
226
233
|
name,
|
|
227
234
|
http: http === true,
|
|
228
235
|
gui: gui === true,
|
|
236
|
+
cli: cli === true,
|
|
229
237
|
noSession: noSession === true,
|
|
230
238
|
};
|
|
239
|
+
if (prompt !== null && prompt !== undefined) {
|
|
240
|
+
options.prompt = prompt;
|
|
241
|
+
}
|
|
231
242
|
if (port !== null && port !== undefined) {
|
|
232
243
|
options.port = port;
|
|
233
244
|
}
|
|
234
245
|
await runCommand(options);
|
|
246
|
+
})
|
|
247
|
+
.with({ command: "batch" }, async ({ name, queries, file, concurrency, port }) => {
|
|
248
|
+
const options = {
|
|
249
|
+
name,
|
|
250
|
+
};
|
|
251
|
+
if (queries.length > 0) {
|
|
252
|
+
options.queries = [...queries]; // Convert readonly array to mutable
|
|
253
|
+
}
|
|
254
|
+
if (file !== null && file !== undefined) {
|
|
255
|
+
options.file = file;
|
|
256
|
+
}
|
|
257
|
+
if (concurrency !== null && concurrency !== undefined) {
|
|
258
|
+
options.concurrency = concurrency;
|
|
259
|
+
}
|
|
260
|
+
if (port !== null && port !== undefined) {
|
|
261
|
+
options.port = port;
|
|
262
|
+
}
|
|
263
|
+
await batchCommand(options);
|
|
235
264
|
})
|
|
236
265
|
.with({ command: "secret" }, async ({ subcommand }) => {
|
|
237
266
|
await match(subcommand)
|
package/dist/lib/auth-fetch.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { type AuthCredentials } from "@townco/core/auth";
|
|
2
1
|
/**
|
|
3
|
-
* Get valid credentials, refreshing if necessary
|
|
4
|
-
*
|
|
2
|
+
* Get valid credentials, refreshing if necessary.
|
|
3
|
+
* Checks disk credentials first, then falls back to SHED_API_KEY env var.
|
|
4
|
+
* Throws if not logged in or refresh fails.
|
|
5
5
|
*/
|
|
6
|
-
export declare function getValidCredentials(): Promise<
|
|
6
|
+
export declare function getValidCredentials(): Promise<{
|
|
7
|
+
accessToken: string;
|
|
8
|
+
shedUrl: string;
|
|
9
|
+
}>;
|
|
7
10
|
/**
|
|
8
11
|
* Make an authenticated fetch request to shed
|
|
9
|
-
* Automatically refreshes token if expired
|
|
10
12
|
*/
|
|
11
13
|
export declare function authFetch(path: string, options?: RequestInit): Promise<Response>;
|
|
12
14
|
/**
|
package/dist/lib/auth-fetch.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isTokenExpired,
|
|
1
|
+
import { getShedUrl, isTokenExpired, loadAuthCredentialsFromDisk, saveAuthCredentials, } from "@townco/core/auth";
|
|
2
2
|
// ============================================================================
|
|
3
3
|
// Token Refresh
|
|
4
4
|
// ============================================================================
|
|
@@ -32,52 +32,53 @@ async function refreshAccessToken(credentials) {
|
|
|
32
32
|
// Public API
|
|
33
33
|
// ============================================================================
|
|
34
34
|
/**
|
|
35
|
-
* Get valid credentials, refreshing if necessary
|
|
36
|
-
*
|
|
35
|
+
* Get valid credentials, refreshing if necessary.
|
|
36
|
+
* Checks disk credentials first, then falls back to SHED_API_KEY env var.
|
|
37
|
+
* Throws if not logged in or refresh fails.
|
|
37
38
|
*/
|
|
38
39
|
export async function getValidCredentials() {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
40
|
+
// Try disk credentials first
|
|
41
|
+
const credentials = loadAuthCredentialsFromDisk();
|
|
42
|
+
if (credentials) {
|
|
43
|
+
if (isTokenExpired(credentials)) {
|
|
44
|
+
try {
|
|
45
|
+
const refreshed = await refreshAccessToken(credentials);
|
|
46
|
+
return {
|
|
47
|
+
accessToken: refreshed.access_token,
|
|
48
|
+
shedUrl: refreshed.shed_url,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw new Error(`Session expired. Please run 'town login' again. (${error instanceof Error ? error.message : String(error)})`);
|
|
53
|
+
}
|
|
49
54
|
}
|
|
55
|
+
return {
|
|
56
|
+
accessToken: credentials.access_token,
|
|
57
|
+
shedUrl: credentials.shed_url,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// Fall back to SHED_API_KEY
|
|
61
|
+
const apiKey = process.env.SHED_API_KEY;
|
|
62
|
+
if (apiKey) {
|
|
63
|
+
return {
|
|
64
|
+
accessToken: apiKey,
|
|
65
|
+
shedUrl: getShedUrl(),
|
|
66
|
+
};
|
|
50
67
|
}
|
|
51
|
-
|
|
68
|
+
throw new Error("Not logged in. Run 'town login' first.");
|
|
52
69
|
}
|
|
53
70
|
/**
|
|
54
71
|
* Make an authenticated fetch request to shed
|
|
55
|
-
* Automatically refreshes token if expired
|
|
56
72
|
*/
|
|
57
73
|
export async function authFetch(path, options = {}) {
|
|
58
|
-
|
|
59
|
-
const url = path.startsWith("http") ? path : `${credentials.
|
|
74
|
+
const credentials = await getValidCredentials();
|
|
75
|
+
const url = path.startsWith("http") ? path : `${credentials.shedUrl}${path}`;
|
|
60
76
|
const headers = new Headers(options.headers);
|
|
61
|
-
headers.set("Authorization", `Bearer ${credentials.
|
|
62
|
-
|
|
77
|
+
headers.set("Authorization", `Bearer ${credentials.accessToken}`);
|
|
78
|
+
return fetch(url, {
|
|
63
79
|
...options,
|
|
64
80
|
headers,
|
|
65
81
|
});
|
|
66
|
-
// If we get 401, try refreshing once
|
|
67
|
-
if (response.status === 401) {
|
|
68
|
-
try {
|
|
69
|
-
credentials = await refreshAccessToken(credentials);
|
|
70
|
-
headers.set("Authorization", `Bearer ${credentials.access_token}`);
|
|
71
|
-
return fetch(url, {
|
|
72
|
-
...options,
|
|
73
|
-
headers,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
throw new Error("Session expired. Please run 'town login' again.");
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return response;
|
|
81
82
|
}
|
|
82
83
|
/**
|
|
83
84
|
* Convenience method for GET requests
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@townco/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.74",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"town": "./dist/index.js"
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"build": "tsc"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@townco/tsconfig": "0.1.
|
|
18
|
+
"@townco/tsconfig": "0.1.66",
|
|
19
19
|
"@types/archiver": "^7.0.0",
|
|
20
20
|
"@types/bun": "^1.3.1",
|
|
21
21
|
"@types/ignore-walk": "^4.0.3",
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@optique/core": "^0.6.2",
|
|
27
27
|
"@optique/run": "^0.6.2",
|
|
28
|
-
"@townco/agent": "0.1.
|
|
29
|
-
"@townco/core": "0.0.
|
|
30
|
-
"@townco/debugger": "0.1.
|
|
31
|
-
"@townco/env": "0.1.
|
|
32
|
-
"@townco/secret": "0.1.
|
|
33
|
-
"@townco/ui": "0.1.
|
|
28
|
+
"@townco/agent": "0.1.74",
|
|
29
|
+
"@townco/core": "0.0.47",
|
|
30
|
+
"@townco/debugger": "0.1.24",
|
|
31
|
+
"@townco/env": "0.1.19",
|
|
32
|
+
"@townco/secret": "0.1.69",
|
|
33
|
+
"@townco/ui": "0.1.69",
|
|
34
34
|
"@trpc/client": "^11.7.2",
|
|
35
35
|
"@types/inquirer": "^9.0.9",
|
|
36
36
|
"@types/ws": "^8.5.13",
|