@townco/cli 0.1.104 → 0.1.105

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.
@@ -48,14 +48,9 @@ const AVAILABLE_TOOLS = [
48
48
  description: "Exa-powered web search (requires EXA_API_KEY)",
49
49
  },
50
50
  {
51
- label: "Filesystem",
52
- value: "filesystem",
53
- description: "Read, write, and search files in the project directory",
54
- },
55
- {
56
- label: "Generate Image",
57
- value: "generate_image",
58
- description: "Generate images using Google's Gemini model (requires GEMINI_API_KEY)",
51
+ label: "Code Sandbox",
52
+ value: "code_sandbox",
53
+ description: "Execute code, run commands, generate images, and manage files in a secure cloud sandbox",
59
54
  },
60
55
  {
61
56
  label: "Browser",
@@ -21,13 +21,6 @@ function TuiRunner({ agentPath, workingDir, agentName, noSession, onExit, }) {
21
21
  useMemo(() => {
22
22
  configureLogsDir(join(workingDir, ".logs"));
23
23
  }, [workingDir]);
24
- // Session start time for log filtering - set once when component mounts
25
- // Set to 5 seconds in the past to capture logs generated during initialization
26
- const sessionStartTime = useMemo(() => {
27
- const now = new Date();
28
- now.setSeconds(now.getSeconds() - 5);
29
- return now.toISOString();
30
- }, []);
31
24
  // Create logger with agent directory as logs location
32
25
  const logger = useMemo(() => createLogger("tui", "debug"), []);
33
26
  useEffect(() => {
@@ -84,9 +77,9 @@ function TuiRunner({ agentPath, workingDir, agentName, noSession, onExit, }) {
84
77
  {
85
78
  name: "Logs",
86
79
  type: "custom",
87
- render: () => (_jsx(LogsPane, { logsDir: join(workingDir, ".logs"), sessionStartTime: sessionStartTime })),
80
+ render: () => _jsx(LogsPane, { logsDir: join(workingDir, ".logs") }),
88
81
  },
89
- ], [client, error, workingDir, sessionStartTime]);
82
+ ], [client, error, workingDir]);
90
83
  return (_jsx(TabbedOutput, { processes: [], customTabs: customTabs, onExit: onExit }));
91
84
  }
92
85
  function GuiRunner({ agentProcess, guiProcess, agentPort, agentPath, logger, onExit, }) {
@@ -390,7 +383,7 @@ export async function runCommand(options) {
390
383
  const agentPath = join(projectRoot, "agents", name);
391
384
  // Load agent definition to get displayName and detect MCPs by parsing the file
392
385
  let agentDisplayName = name; // Fallback to agent directory name
393
- let usesBibliotechaMcp = false;
386
+ let usesLibraryMcp = false;
394
387
  try {
395
388
  const agentIndexPath = join(agentPath, "index.ts");
396
389
  const content = await readFile(agentIndexPath, "utf-8");
@@ -400,9 +393,9 @@ export async function runCommand(options) {
400
393
  if (displayNameMatch?.[1]) {
401
394
  agentDisplayName = displayNameMatch[1];
402
395
  }
403
- // Detect if agent uses bibliotecha MCP
404
- // Looking for patterns like: name: "bibliotecha" or name: 'bibliotecha'
405
- usesBibliotechaMcp = /name:\s*["']bibliotecha["']/.test(content);
396
+ // Detect if agent uses library MCP
397
+ // Looking for patterns like: name: "library" or name: 'library'
398
+ usesLibraryMcp = /name:\s*["']library["']/.test(content);
406
399
  }
407
400
  catch (_error) {
408
401
  // If we can't read the agent definition, just use the directory name
@@ -525,11 +518,11 @@ export async function runCommand(options) {
525
518
  ...configEnvVars,
526
519
  VITE_AGENT_URL: `${process.env.EXT_HOST || "http://localhost"}:${port}`,
527
520
  VITE_DEBUGGER_URL: `http://localhost:${debuggerUiPort}`,
528
- // If agent uses bibliotecha MCP, pass BIBLIOTECHA_URL to GUI for auth
529
- ...(usesBibliotechaMcp &&
530
- process.env.BIBLIOTECHA_URL && {
531
- VITE_BIBLIOTECHA_URL: process.env.BIBLIOTECHA_URL,
532
- VITE_BIBLIOTECHA_FRONTEND_URL: process.env.BIBLIOTECHA_FRONTEND_URL,
521
+ // If agent uses library MCP, pass LIBRARY_API_URL to GUI for auth
522
+ ...(usesLibraryMcp &&
523
+ process.env.LIBRARY_API_URL && {
524
+ VITE_LIBRARY_API_URL: process.env.LIBRARY_API_URL,
525
+ VITE_LIBRARY_FRONTEND_URL: process.env.LIBRARY_FRONTEND_URL,
533
526
  }),
534
527
  },
535
528
  });
@@ -1,5 +1,4 @@
1
1
  export interface LogsPaneProps {
2
2
  logsDir?: string;
3
- sessionStartTime?: string;
4
3
  }
5
- export declare function LogsPane({ logsDir: customLogsDir, sessionStartTime: providedSessionStartTime }?: LogsPaneProps): import("react/jsx-runtime").JSX.Element;
4
+ export declare function LogsPane({ logsDir: customLogsDir }?: LogsPaneProps): import("react/jsx-runtime").JSX.Element;
@@ -1,262 +1,166 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { spawn } from "node:child_process";
3
- import { existsSync, readdirSync } from "node:fs";
3
+ import { existsSync } from "node:fs";
4
4
  import { join } from "node:path";
5
+ import { LOG_FILE_NAME } from "@townco/core";
5
6
  import { Box, Text, useInput } from "ink";
6
7
  import TextInput from "ink-text-input";
7
- import { useEffect, useMemo, useState } from "react";
8
- const LOG_COLORS = {
9
- trace: "gray",
10
- debug: "blue",
11
- info: "green",
12
- warn: "yellow",
13
- error: "red",
14
- fatal: "redBright",
15
- };
16
- const SERVICE_COLORS = ["cyan", "magenta", "blue", "green", "yellow"];
17
- // Fuzzy search: checks if query characters appear in order in the target string
18
- function fuzzyMatch(query, target) {
19
- if (!query)
20
- return true;
21
- const lowerQuery = query.toLowerCase();
22
- const lowerTarget = target.toLowerCase();
23
- let queryIndex = 0;
24
- for (let i = 0; i < lowerTarget.length && queryIndex < lowerQuery.length; i++) {
25
- if (lowerTarget[i] === lowerQuery[queryIndex]) {
26
- queryIndex++;
27
- }
8
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
9
+ const LOG_LEVELS = [
10
+ "all",
11
+ "debug",
12
+ "info",
13
+ "warn",
14
+ "error",
15
+ "fatal",
16
+ ];
17
+ // Max lines to keep in buffer
18
+ const MAX_LINES = 1000;
19
+ function buildCommand(logsDir, pretty, level, sessionStartTime) {
20
+ const logFile = join(logsDir, LOG_FILE_NAME);
21
+ const tailCmd = `tail -n +1 -f "${logFile}"`;
22
+ // Build jq filter conditions
23
+ const conditions = [];
24
+ conditions.push(`.timestamp >= "${sessionStartTime}"`);
25
+ if (level !== "all") {
26
+ const levelOrder = ["debug", "info", "warn", "error", "fatal"];
27
+ const levelIndex = levelOrder.indexOf(level);
28
+ const allowedLevels = levelOrder.slice(levelIndex);
29
+ const levelCondition = allowedLevels
30
+ .map((l) => `.level == "${l}"`)
31
+ .join(" or ");
32
+ conditions.push(`(${levelCondition})`);
28
33
  }
29
- return queryIndex === lowerQuery.length;
34
+ const jqFilter = `select(${conditions.join(" and ")})`;
35
+ const jqFlags = pretty ? "-C --unbuffered" : "-c --unbuffered";
36
+ return `${tailCmd} | jq ${jqFlags} '${jqFilter}'`;
30
37
  }
31
- export function LogsPane({ logsDir: customLogsDir, sessionStartTime: providedSessionStartTime, } = {}) {
32
- const [logLines, setLogLines] = useState([]);
33
- const [serviceFilter, setServiceFilter] = useState(null);
34
- const [levelFilter, setLevelFilter] = useState(null);
35
- const [availableServices, setAvailableServices] = useState([]);
36
- const [scrollOffset, setScrollOffset] = useState(0); // 0 = at bottom, positive = scrolled up
37
- const [searchMode, setSearchMode] = useState(false);
38
- const [searchQuery, setSearchQuery] = useState("");
39
- const [expandedLogs, setExpandedLogs] = useState(false); // Start collapsed
38
+ export function LogsPane({ logsDir: customLogsDir } = {}) {
39
+ const logsDir = useMemo(() => customLogsDir ?? join(process.cwd(), ".logs"), [customLogsDir]);
40
+ const sessionStartTime = useMemo(() => new Date().toISOString(), []);
41
+ const [pretty, setPretty] = useState(true);
42
+ const [level, setLevel] = useState("all");
43
+ const [editMode, setEditMode] = useState(false);
44
+ const [customCommand, setCustomCommand] = useState("");
45
+ const [outputLines, setOutputLines] = useState([]);
46
+ const [error, setError] = useState(null);
40
47
  const [terminalHeight, setTerminalHeight] = useState(process.stdout.rows ?? 30);
48
+ const processRef = useRef(null);
49
+ const commandRunIdRef = useRef(0);
50
+ const activeCommand = useMemo(() => editMode
51
+ ? customCommand
52
+ : buildCommand(logsDir, pretty, level, sessionStartTime), [editMode, customCommand, logsDir, pretty, level, sessionStartTime]);
41
53
  // Track terminal resize
42
54
  useEffect(() => {
43
- const handleResize = () => {
44
- setTerminalHeight(process.stdout.rows ?? 30);
45
- };
55
+ const handleResize = () => setTerminalHeight(process.stdout.rows ?? 30);
46
56
  process.stdout.on("resize", handleResize);
47
57
  return () => {
48
58
  process.stdout.off("resize", handleResize);
49
59
  };
50
60
  }, []);
51
- // Use provided session start time or default to 1 second before component mount
52
- const sessionStartTime = useMemo(() => {
53
- if (providedSessionStartTime)
54
- return providedSessionStartTime;
55
- const now = new Date();
56
- now.setSeconds(now.getSeconds() - 1);
57
- return now.toISOString();
58
- }, [providedSessionStartTime]);
59
- const logsDir = customLogsDir ?? join(process.cwd(), ".logs");
60
- // Update available services based on logs from current session
61
- useEffect(() => {
62
- const servicesWithLogs = new Set(logLines.map((log) => log.service));
63
- const newServices = Array.from(servicesWithLogs);
64
- setAvailableServices(newServices);
65
- // Clear service filter if the selected service is no longer available
66
- if (serviceFilter && !newServices.includes(serviceFilter)) {
67
- setServiceFilter(null);
68
- }
69
- }, [logLines, serviceFilter]);
70
- // Tail all log files and merge output
61
+ // Spawn tail process
71
62
  useEffect(() => {
72
63
  if (!existsSync(logsDir)) {
73
- setLogLines([
74
- {
75
- timestamp: new Date().toISOString(),
76
- level: "info",
77
- service: "logs",
78
- message: `No logs directory found at ${logsDir}`,
79
- },
80
- ]);
64
+ setError(`Logs directory not found: ${logsDir}`);
81
65
  return;
82
66
  }
83
- const logFiles = readdirSync(logsDir).filter((f) => f.endsWith(".log"));
84
- if (logFiles.length === 0) {
85
- setLogLines([
86
- {
87
- timestamp: new Date().toISOString(),
88
- level: "info",
89
- service: "logs",
90
- message: "No log files found. Logs will appear here once services start logging.",
91
- },
92
- ]);
93
- return;
67
+ if (processRef.current) {
68
+ processRef.current.kill();
69
+ processRef.current = null;
94
70
  }
95
- // Tail all log files (get last 50 lines, then follow)
96
- const tailProcesses = logFiles.map((file) => {
97
- const filePath = join(logsDir, file);
98
- const tail = spawn("tail", ["-f", "-n", "50", filePath]);
99
- tail.stdout.on("data", (data) => {
100
- const lines = data
101
- .toString()
102
- .split("\n")
103
- .filter((line) => line.trim());
104
- const entries = lines
105
- .map((line) => {
106
- try {
107
- const parsed = JSON.parse(line);
108
- return parsed;
109
- }
110
- catch {
111
- // If not JSON, create a simple entry
112
- return {
113
- timestamp: new Date().toISOString(),
114
- level: "info",
115
- service: file.replace(".log", ""),
116
- message: line,
117
- _raw: line,
118
- };
119
- }
120
- })
121
- .filter((entry) => entry.timestamp >= sessionStartTime);
122
- setLogLines((prev) => {
123
- const combined = [...prev, ...entries];
124
- // Sort by timestamp and keep last 200 entries
125
- const sorted = combined.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
126
- return sorted.slice(-200);
127
- });
71
+ // Clear output when command changes
72
+ setOutputLines([]);
73
+ const runId = ++commandRunIdRef.current;
74
+ const timeoutId = setTimeout(() => {
75
+ if (runId !== commandRunIdRef.current)
76
+ return;
77
+ const proc = spawn("sh", ["-c", activeCommand], {
78
+ cwd: logsDir,
79
+ env: { ...process.env, TERM: "xterm-256color" },
128
80
  });
129
- return tail;
130
- });
131
- // Cleanup
81
+ processRef.current = proc;
82
+ proc.stdout?.on("data", (data) => {
83
+ if (runId !== commandRunIdRef.current)
84
+ return;
85
+ const lines = data.toString().split("\n").filter(Boolean);
86
+ setOutputLines((prev) => [...prev, ...lines].slice(-MAX_LINES));
87
+ });
88
+ proc.stderr?.on("data", (data) => {
89
+ if (runId !== commandRunIdRef.current)
90
+ return;
91
+ const text = data.toString().trim();
92
+ if (text && !text.includes("parse error")) {
93
+ setOutputLines((prev) => [...prev, `[stderr] ${text}`].slice(-MAX_LINES));
94
+ }
95
+ });
96
+ proc.on("error", (err) => {
97
+ if (runId !== commandRunIdRef.current)
98
+ return;
99
+ setError(`Failed to start: ${err.message}`);
100
+ });
101
+ }, 50);
132
102
  return () => {
133
- for (const tail of tailProcesses) {
134
- tail.kill();
103
+ clearTimeout(timeoutId);
104
+ if (processRef.current) {
105
+ processRef.current.kill();
106
+ processRef.current = null;
135
107
  }
136
108
  };
137
- }, [logsDir, sessionStartTime]);
138
- // Apply filters
139
- const filteredLogs = logLines.filter((log) => {
140
- if (serviceFilter && log.service !== serviceFilter) {
141
- return false;
142
- }
143
- if (levelFilter) {
144
- const levelOrder = [
145
- "trace",
146
- "debug",
147
- "info",
148
- "warn",
149
- "error",
150
- "fatal",
151
- ];
152
- const logLevelIndex = levelOrder.indexOf(log.level);
153
- const filterLevelIndex = levelOrder.indexOf(levelFilter);
154
- if (logLevelIndex < filterLevelIndex) {
155
- return false;
156
- }
157
- }
158
- return true;
159
- });
160
- // Handle keyboard input for filtering and scrolling
161
- useInput((input, key) => {
162
- // '/' to enter search mode
163
- if (input === "/" && !searchMode) {
164
- setSearchMode(true);
109
+ }, [activeCommand, logsDir]);
110
+ const handleInput = useCallback((input, key) => {
111
+ if (key.ctrl && input === "c")
165
112
  return;
166
- }
167
- // Escape to exit search mode or jump to bottom
168
- if (key.escape) {
169
- if (searchMode) {
170
- setSearchMode(false);
171
- setSearchQuery("");
113
+ if (editMode) {
114
+ if (key.escape) {
115
+ setEditMode(false);
116
+ return;
172
117
  }
173
- else {
174
- setScrollOffset(0);
118
+ if (key.return) {
119
+ setEditMode(false);
120
+ return;
175
121
  }
176
122
  return;
177
123
  }
178
- // Don't handle other inputs in search mode (let TextInput handle them)
179
- if (searchMode) {
124
+ if (input === "e") {
125
+ setCustomCommand(activeCommand);
126
+ setEditMode(true);
180
127
  return;
181
128
  }
182
- // 's' to cycle through service filters
183
- if (input === "s") {
184
- if (serviceFilter === null) {
185
- setServiceFilter(availableServices[0] ?? null);
186
- }
187
- else {
188
- const currentIndex = availableServices.indexOf(serviceFilter);
189
- const nextIndex = (currentIndex + 1) % (availableServices.length + 1);
190
- setServiceFilter(nextIndex === availableServices.length
191
- ? null
192
- : (availableServices[nextIndex] ?? null));
193
- }
129
+ if (input === "f") {
130
+ setPretty((prev) => !prev);
131
+ return;
194
132
  }
195
- // 'l' to cycle through level filters
196
133
  if (input === "l") {
197
- const levels = [
198
- null,
199
- "debug",
200
- "info",
201
- "warn",
202
- "error",
203
- "fatal",
204
- ];
205
- const currentIndex = levelFilter === null ? 0 : levels.indexOf(levelFilter);
206
- const nextIndex = (currentIndex + 1) % levels.length;
207
- const nextLevel = levels[nextIndex];
208
- setLevelFilter(nextLevel ?? null);
209
- }
210
- // 'c' to clear all filters, logs, and reset scroll
211
- if (input === "c") {
212
- setServiceFilter(null);
213
- setLevelFilter(null);
214
- setLogLines([]); // Clear the log buffer
215
- setScrollOffset(0); // Also jump to bottom when clearing
134
+ setLevel((prev) => {
135
+ const currentIndex = LOG_LEVELS.indexOf(prev);
136
+ const nextIndex = (currentIndex + 1) % LOG_LEVELS.length;
137
+ return LOG_LEVELS[nextIndex] ?? "all";
138
+ });
216
139
  return;
217
140
  }
218
- // 'e' to toggle expanded/collapsed metadata view
219
- if (input === "e") {
220
- setExpandedLogs((prev) => !prev);
141
+ if (input === "c" && !key.ctrl) {
142
+ setOutputLines([]);
221
143
  return;
222
144
  }
223
- });
224
- // Apply fuzzy search if active
225
- const searchedLogs = searchQuery
226
- ? filteredLogs.filter((log) => fuzzyMatch(searchQuery, log.message))
227
- : filteredLogs;
228
- // Calculate display window based on terminal height
229
- // Reserve space for: status line (3), header (3), search box if active (3), footer (3), padding (1)
230
- const reservedLines = 10 + (searchMode ? 3 : 0);
231
- const availableLines = Math.max(10, terminalHeight - reservedLines);
232
- // When expanded, each log entry with metadata takes ~5 lines on average
233
- // When collapsed, each log entry takes 1 line
234
- const avgLinesPerEntry = expandedLogs ? 5 : 1;
235
- const maxLines = Math.floor(availableLines / avgLinesPerEntry);
236
- const totalLogs = searchedLogs.length;
237
- const isAtBottom = scrollOffset === 0;
238
- // Calculate which slice of logs to show
239
- const displayLogs = isAtBottom
240
- ? searchedLogs.slice(-maxLines) // Show last N lines when at bottom
241
- : searchedLogs.slice(Math.max(0, totalLogs - maxLines - scrollOffset), totalLogs - scrollOffset);
242
- const _canScrollUp = totalLogs > maxLines && scrollOffset < totalLogs - maxLines;
243
- const _canScrollDown = scrollOffset > 0;
244
- return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [searchMode && (_jsxs(Box, { borderStyle: "single", borderBottom: true, borderColor: "cyan", paddingX: 1, children: [
245
- _jsx(Text, { color: "cyan", children: "Search: " }), _jsx(TextInput, { value: searchQuery, onChange: setSearchQuery, placeholder: "Type to search..." }), _jsx(Text, { dimColor: true, children: " (ESC to exit)" })
246
- ] })), _jsx(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: displayLogs.length === 0 ? (_jsx(Text, { dimColor: true, children: "No logs match the current filters. Press 'c' to clear filters." })) : (displayLogs.map((log, idx) => {
247
- const serviceColor = SERVICE_COLORS[availableServices.indexOf(log.service) % SERVICE_COLORS.length] || "white";
248
- const levelColor = LOG_COLORS[log.level] || "white";
249
- const time = new Date(log.timestamp).toLocaleTimeString();
250
- const keyStr = `${log.timestamp}-${idx}`;
251
- return (_jsxs(Box, { flexDirection: "column", children: [
145
+ }, [editMode, activeCommand]);
146
+ useInput(handleInput);
147
+ if (error) {
148
+ return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(Text, { color: "red", children: error }) }));
149
+ }
150
+ // Calculate visible lines based on terminal height
151
+ // Reserve: footer (4 lines) + status bar (3 lines) + padding
152
+ const visibleLines = Math.max(5, terminalHeight - 10);
153
+ const displayLines = outputLines.slice(-visibleLines);
154
+ return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [
155
+ _jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: displayLines.length === 0 ? (_jsx(Text, { dimColor: true, children: "Waiting for logs..." })) : (displayLines.map((line, idx) => (_jsx(Text, { children: line }, `${idx}-${line.slice(0, 20)}`)))) }), _jsxs(Box, { borderStyle: "single", borderTop: true, borderColor: "gray", paddingX: 1, flexDirection: "column", flexShrink: 0, children: [editMode ? (_jsxs(Box, { children: [
156
+ _jsx(Text, { color: "cyan", children: "$ " }), _jsx(TextInput, { value: customCommand, onChange: setCustomCommand, placeholder: "Enter command..." }), _jsx(Text, { dimColor: true, children: " (Enter to apply, Esc to cancel)" })
157
+ ] })) : (_jsxs(Box, { children: [
158
+ _jsx(Text, { dimColor: true, children: "$ " }), _jsx(Text, { children: activeCommand })
159
+ ] })), _jsxs(Box, { justifyContent: "space-between", children: [
252
160
  _jsxs(Box, { children: [
253
- _jsxs(Text, { dimColor: true, children: ["[", time, "]"] }), _jsxs(Text, { color: serviceColor, children: [" [", log.service, "]"] }), _jsxs(Text, { color: levelColor, children: [" [", log.level.toUpperCase(), "]"] }), _jsxs(Text, { children: [" ", log.message] })
254
- ] }), expandedLogs &&
255
- log.metadata &&
256
- Object.keys(log.metadata).length > 0 && (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: JSON.stringify(log.metadata, null, 2) }) }))] }, keyStr));
257
- })) }), _jsxs(Box, { borderStyle: "single", borderTop: true, borderColor: "gray", paddingX: 1, justifyContent: "space-between", children: [
258
- _jsxs(Box, { children: [serviceFilter && _jsxs(Text, { children: ["[", serviceFilter, "] "] }), levelFilter && (_jsxs(Text, { children: ["[", ">=", levelFilter, "]", " "] })), searchQuery && _jsxs(Text, { color: "cyan", children: ["[SEARCH: ", searchQuery, "] "] }), !isAtBottom && _jsx(Text, { color: "yellow", children: "[SCROLLED] " }), expandedLogs && _jsx(Text, { color: "green", children: "[EXPANDED] " }), _jsxs(Text, { dimColor: true, children: [displayLogs.length, "/", totalLogs, " logs", scrollOffset > 0 && ` (${scrollOffset} from bottom)`] })
259
- ] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "(/)search | (s)ervice | (l)evel | (e)xpand | (c)lear | ESC" }) })
161
+ _jsxs(Text, { ...(pretty && { color: "green" }), children: ["[", pretty ? "formatted" : "raw", "]"] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: "cyan", children: ["[level: ", level, "]"] }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [outputLines.length, " lines"] })
162
+ ] }), _jsx(Text, { dimColor: true, children: "(e)dit | (f)ormat | (l)evel | (c)lear" })
163
+ ] })
260
164
  ] })
261
165
  ] }));
262
166
  }
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { appendFileSync, existsSync, mkdirSync } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import { LOG_FILE_NAME } from "@townco/core";
4
5
  import { Box, Text, useApp, useInput } from "ink";
5
6
  import { useEffect, useRef, useState } from "react";
6
7
  import { LogsPane } from "./LogsPane.js";
@@ -71,7 +72,7 @@ export function TabbedOutput({ processes, customTabs = [], logsDir, onExit, onPo
71
72
  markAsRunning();
72
73
  // Write to log file if logsDir is provided
73
74
  if (logsDir) {
74
- const logFile = join(logsDir, `${processInfo.name.toLowerCase()}.log`);
75
+ const logFile = join(logsDir, LOG_FILE_NAME);
75
76
  try {
76
77
  const timestamp = new Date().toISOString();
77
78
  const logEntry = JSON.stringify({
@@ -115,7 +116,7 @@ export function TabbedOutput({ processes, customTabs = [], logsDir, onExit, onPo
115
116
  markAsRunning();
116
117
  // Write to log file if logsDir is provided
117
118
  if (logsDir) {
118
- const logFile = join(logsDir, `${processInfo.name.toLowerCase()}.log`);
119
+ const logFile = join(logsDir, LOG_FILE_NAME);
119
120
  try {
120
121
  const timestamp = new Date().toISOString();
121
122
  const logEntry = JSON.stringify({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/cli",
3
- "version": "0.1.104",
3
+ "version": "0.1.105",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "town": "./dist/index.js"
@@ -15,7 +15,7 @@
15
15
  "build": "tsgo"
16
16
  },
17
17
  "devDependencies": {
18
- "@townco/tsconfig": "0.1.96",
18
+ "@townco/tsconfig": "0.1.97",
19
19
  "@types/archiver": "^7.0.0",
20
20
  "@types/bun": "^1.3.1",
21
21
  "@types/ignore-walk": "^4.0.3",
@@ -24,13 +24,13 @@
24
24
  "dependencies": {
25
25
  "@optique/core": "^0.6.2",
26
26
  "@optique/run": "^0.6.2",
27
- "@townco/agent": "0.1.107",
28
- "@townco/apiclient": "0.0.19",
29
- "@townco/core": "0.0.77",
30
- "@townco/debugger": "0.1.55",
31
- "@townco/env": "0.1.49",
32
- "@townco/secret": "0.1.99",
33
- "@townco/ui": "0.1.99",
27
+ "@townco/agent": "0.1.108",
28
+ "@townco/apiclient": "0.0.20",
29
+ "@townco/core": "0.0.78",
30
+ "@townco/debugger": "0.1.56",
31
+ "@townco/env": "0.1.50",
32
+ "@townco/secret": "0.1.100",
33
+ "@townco/ui": "0.1.100",
34
34
  "@trpc/client": "^11.7.2",
35
35
  "archiver": "^7.0.1",
36
36
  "eventsource": "^4.1.0",