@townco/cli 0.1.50 → 0.1.52

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.
@@ -101,7 +101,7 @@ function CreateApp({ name: initialName, model: initialModel, tools: initialTools
101
101
  {
102
102
  type: "context_size",
103
103
  setting: {
104
- threshold: 95,
104
+ threshold: 80,
105
105
  },
106
106
  callback: "compaction_tool",
107
107
  },
@@ -1,14 +1,9 @@
1
1
  interface MCPAddProps {
2
- name?: string;
3
- url?: string;
4
- command?: string;
5
- args?: readonly string[];
2
+ name?: string;
3
+ url?: string;
4
+ command?: string;
5
+ args?: readonly string[];
6
6
  }
7
- declare function MCPAddApp({
8
- name: initialName,
9
- url: initialUrl,
10
- command: initialCommand,
11
- args: initialArgs,
12
- }: MCPAddProps): import("react/jsx-runtime").JSX.Element | null;
7
+ declare function MCPAddApp({ name: initialName, url: initialUrl, command: initialCommand, args: initialArgs, }: MCPAddProps): import("react/jsx-runtime").JSX.Element | null;
13
8
  export default MCPAddApp;
14
9
  export declare function runMCPAdd(props?: MCPAddProps): Promise<void>;
@@ -2,7 +2,7 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { spawn } from "node:child_process";
3
3
  import { existsSync } from "node:fs";
4
4
  import { readFile } from "node:fs/promises";
5
- import { join } from "node:path";
5
+ import { dirname, join } from "node:path";
6
6
  import { isInsideTownProject } from "@townco/agent/storage";
7
7
  import { configureLogsDir, createLogger } from "@townco/core";
8
8
  import { AcpClient } from "@townco/ui";
@@ -38,7 +38,10 @@ function TuiRunner({ agentPath, workingDir, noSession, onExit, }) {
38
38
  options: {
39
39
  agentPath,
40
40
  workingDirectory: workingDir,
41
- environment: noSession ? { TOWN_NO_SESSION: "true" } : {},
41
+ environment: {
42
+ ENABLE_TELEMETRY: "true",
43
+ ...(noSession ? { TOWN_NO_SESSION: "true" } : {}),
44
+ },
42
45
  },
43
46
  });
44
47
  setClient(newClient);
@@ -156,6 +159,25 @@ export async function runCommand(options) {
156
159
  }
157
160
  // Load environment variables from project .env
158
161
  const configEnvVars = await loadEnvVars(projectRoot);
162
+ // Start the debugger server as subprocess (OTLP collector + UI)
163
+ const debuggerPkgPath = require.resolve("@townco/debugger/package.json");
164
+ const debuggerDir = dirname(debuggerPkgPath);
165
+ const debuggerProcess = spawn("bun", ["src/index.ts"], {
166
+ cwd: debuggerDir,
167
+ stdio: "inherit",
168
+ env: {
169
+ ...process.env,
170
+ DB_PATH: join(projectRoot, ".traces.db"),
171
+ },
172
+ });
173
+ console.log(`Debugger UI: http://localhost:4000`);
174
+ // Cleanup debugger process on exit
175
+ const cleanupDebugger = () => {
176
+ debuggerProcess.kill();
177
+ };
178
+ process.on("exit", cleanupDebugger);
179
+ process.on("SIGINT", cleanupDebugger);
180
+ process.on("SIGTERM", cleanupDebugger);
159
181
  // Resolve agent path within the project
160
182
  const agentPath = join(projectRoot, "agents", name);
161
183
  // Check if agent exists
@@ -220,6 +242,7 @@ export async function runCommand(options) {
220
242
  ...configEnvVars,
221
243
  NODE_ENV: process.env.NODE_ENV || "production",
222
244
  PORT: availablePort.toString(),
245
+ ENABLE_TELEMETRY: "true",
223
246
  ...(noSession ? { TOWN_NO_SESSION: "true" } : {}),
224
247
  },
225
248
  });
@@ -266,6 +289,7 @@ export async function runCommand(options) {
266
289
  ...configEnvVars,
267
290
  NODE_ENV: process.env.NODE_ENV || "production",
268
291
  PORT: availablePort.toString(),
292
+ ENABLE_TELEMETRY: "true",
269
293
  ...(noSession ? { TOWN_NO_SESSION: "true" } : {}),
270
294
  },
271
295
  });
@@ -19,6 +19,19 @@ function parseLogLevel(line) {
19
19
  }
20
20
  return null;
21
21
  }
22
+ // Parse timestamp from a JSON log line
23
+ function parseTimestamp(line) {
24
+ try {
25
+ const parsed = JSON.parse(line);
26
+ if (parsed.timestamp && typeof parsed.timestamp === "string") {
27
+ return parsed.timestamp;
28
+ }
29
+ }
30
+ catch {
31
+ // Not JSON, use current time
32
+ }
33
+ return new Date().toISOString();
34
+ }
22
35
  // Fuzzy search: checks if query characters appear in order in the target string
23
36
  function fuzzyMatch(query, target) {
24
37
  if (!query)
@@ -48,6 +61,7 @@ export function MergedLogsPane({ services, onClear }) {
48
61
  }
49
62
  }, [availableServices, serviceFilter]);
50
63
  // Merge all outputs into a single array with service tags and parsed levels
64
+ // Sort by timestamp to show chronological order
51
65
  const mergedLines = useMemo(() => {
52
66
  const lines = [];
53
67
  for (const [serviceIndex, service] of services.entries()) {
@@ -58,10 +72,14 @@ export function MergedLogsPane({ services, onClear }) {
58
72
  index,
59
73
  serviceIndex,
60
74
  level: parseLogLevel(line),
75
+ timestamp: parseTimestamp(line),
61
76
  });
62
77
  }
63
78
  }
64
- return lines;
79
+ // Sort by timestamp chronologically
80
+ return lines.sort((a, b) => {
81
+ return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime();
82
+ });
65
83
  }, [services]);
66
84
  // Handle keyboard input
67
85
  useInput((input, key) => {
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { appendFileSync, existsSync, mkdirSync } from "node:fs";
2
+ import { spawn } from "node:child_process";
3
+ import { appendFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
3
4
  import { join } from "node:path";
4
5
  import { Box, useApp, useInput } from "ink";
5
6
  import { useEffect, useRef, useState } from "react";
@@ -20,6 +21,7 @@ export function TabbedOutput({ processes, customTabs = [], logsDir, onExit, onPo
20
21
  const [statuses, setStatuses] = useState(processes.map(() => "starting"));
21
22
  const [ports, setPorts] = useState(processes.map((p) => p.port));
22
23
  const portDetectedRef = useRef(new Set());
24
+ const [logFileOutputs, setLogFileOutputs] = useState({});
23
25
  // Ensure logs directory exists if provided
24
26
  useEffect(() => {
25
27
  if (logsDir && !existsSync(logsDir)) {
@@ -172,6 +174,59 @@ export function TabbedOutput({ processes, customTabs = [], logsDir, onExit, onPo
172
174
  });
173
175
  };
174
176
  }, [processes, onPortDetected, logsDir]);
177
+ // GUI mode: only process tabs, no custom tabs - use merged logs view
178
+ const isGuiMode = customTabs.length === 0 && processes.length > 0;
179
+ // Tail log files in GUI mode to capture internal service logs
180
+ useEffect(() => {
181
+ if (!logsDir || !isGuiMode || !existsSync(logsDir)) {
182
+ return;
183
+ }
184
+ const tailProcesses = [];
185
+ const tailedFiles = new Set();
186
+ // Function to start tailing a specific log file
187
+ const tailLogFile = (file) => {
188
+ if (tailedFiles.has(file))
189
+ return;
190
+ tailedFiles.add(file);
191
+ const serviceName = file.replace(".log", "");
192
+ const filePath = join(logsDir, file);
193
+ // Use tail -F (follow by name, retry if file doesn't exist)
194
+ const tail = spawn("tail", ["-F", "-n", "50", filePath]);
195
+ tail.stdout.on("data", (data) => {
196
+ const lines = data
197
+ .toString()
198
+ .split("\n")
199
+ .filter((line) => line.trim());
200
+ setLogFileOutputs((prev) => ({
201
+ ...prev,
202
+ [serviceName]: [...(prev[serviceName] || []), ...lines],
203
+ }));
204
+ });
205
+ tail.stderr.on("data", (_data) => {
206
+ // Ignore tail errors (like file not found initially)
207
+ });
208
+ tailProcesses.push(tail);
209
+ };
210
+ // Scan for log files and start tailing them
211
+ const scanLogFiles = () => {
212
+ if (!existsSync(logsDir))
213
+ return;
214
+ const logFiles = readdirSync(logsDir).filter((f) => f.endsWith(".log"));
215
+ for (const file of logFiles) {
216
+ tailLogFile(file);
217
+ }
218
+ };
219
+ // Initial scan
220
+ scanLogFiles();
221
+ // Re-scan every 2 seconds to catch new log files
222
+ const scanInterval = setInterval(scanLogFiles, 2000);
223
+ return () => {
224
+ clearInterval(scanInterval);
225
+ for (const tail of tailProcesses) {
226
+ tail.kill();
227
+ }
228
+ };
229
+ }, [logsDir, isGuiMode]);
175
230
  const currentTab = allTabs[activeTab];
176
231
  if (!currentTab) {
177
232
  return null;
@@ -190,8 +245,6 @@ export function TabbedOutput({ processes, customTabs = [], logsDir, onExit, onPo
190
245
  return newOutputs;
191
246
  });
192
247
  };
193
- // GUI mode: only process tabs, no custom tabs - use merged logs view
194
- const isGuiMode = customTabs.length === 0 && processes.length > 0;
195
248
  if (isGuiMode) {
196
249
  const serviceOutputs = processes.map((proc, idx) => ({
197
250
  service: proc.name,
@@ -199,7 +252,15 @@ export function TabbedOutput({ processes, customTabs = [], logsDir, onExit, onPo
199
252
  port: ports[idx],
200
253
  status: statuses[idx] || "starting",
201
254
  }));
202
- return (_jsx(Box, { flexDirection: "column", height: "100%", children: _jsx(MergedLogsPane, { services: serviceOutputs, onClear: handleClearMergedOutput }) }));
255
+ // Add log file services (internal agent services like adapter, agent-runner, etc.)
256
+ const logFileServices = Object.entries(logFileOutputs).map(([service, output]) => ({
257
+ service,
258
+ output,
259
+ port: undefined,
260
+ status: "running",
261
+ }));
262
+ const allServices = [...serviceOutputs, ...logFileServices];
263
+ return (_jsx(Box, { flexDirection: "column", height: "100%", children: _jsx(MergedLogsPane, { services: allServices, onClear: handleClearMergedOutput }) }));
203
264
  }
204
265
  // TUI mode: has custom tabs - use tabbed interface
205
266
  // Get status content from current tab if available
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/cli",
3
- "version": "0.1.50",
3
+ "version": "0.1.52",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "town": "./dist/index.js"
@@ -15,17 +15,18 @@
15
15
  "build": "tsc"
16
16
  },
17
17
  "devDependencies": {
18
- "@townco/tsconfig": "0.1.42",
18
+ "@townco/tsconfig": "0.1.44",
19
19
  "@types/bun": "^1.3.1",
20
20
  "@types/react": "^19.2.2"
21
21
  },
22
22
  "dependencies": {
23
23
  "@optique/core": "^0.6.2",
24
24
  "@optique/run": "^0.6.2",
25
- "@townco/agent": "0.1.50",
26
- "@townco/core": "0.0.23",
27
- "@townco/secret": "0.1.45",
28
- "@townco/ui": "0.1.45",
25
+ "@townco/agent": "0.1.52",
26
+ "@townco/debugger": "0.1.2",
27
+ "@townco/core": "0.0.25",
28
+ "@townco/secret": "0.1.47",
29
+ "@townco/ui": "0.1.47",
29
30
  "@types/inquirer": "^9.0.9",
30
31
  "ink": "^6.4.0",
31
32
  "ink-text-input": "^6.0.0",