|
1 | | -import { readdir, readFile, stat } from "node:fs/promises"; |
| 1 | +import { readdir, readFile, stat, open } from "node:fs/promises"; |
2 | 2 | import { join } from "node:path"; |
3 | 3 | import { homedir } from "node:os"; |
4 | 4 | import { watch } from "node:fs"; |
@@ -163,36 +163,49 @@ export async function tailSession(sessionId?: string): Promise<void> { |
163 | 163 | // ignore |
164 | 164 | } |
165 | 165 |
|
| 166 | + let partialLine = ""; |
| 167 | + |
166 | 168 | const watcher = watch(filePath, async () => { |
167 | 169 | try { |
168 | 170 | const s = await stat(filePath); |
169 | 171 | if (s.size <= lastSize) return; |
170 | 172 |
|
171 | | - const content = await readFile(filePath, "utf-8"); |
172 | | - const lines = content.trim().split("\n").filter(Boolean); |
173 | | - |
174 | | - // Parse only genuinely new lines |
175 | | - const newLines = lines.slice(lastLineCount); |
176 | | - for (const line of newLines) { |
177 | | - try { |
178 | | - const entry = JSON.parse(line) as LogEntry; |
179 | | - console.log(formatEntryLine(entry)); |
180 | | - } catch { |
181 | | - // skip malformed lines during active writing |
| 173 | + // Read only new bytes from the file |
| 174 | + const fd = await open(filePath, "r"); |
| 175 | + try { |
| 176 | + const newBytes = Buffer.alloc(s.size - lastSize); |
| 177 | + await fd.read(newBytes, 0, newBytes.length, lastSize); |
| 178 | + const chunk = partialLine + newBytes.toString("utf-8"); |
| 179 | + const lines = chunk.split("\n"); |
| 180 | + |
| 181 | + // Last element may be a partial line (no trailing newline yet) |
| 182 | + partialLine = lines.pop() ?? ""; |
| 183 | + |
| 184 | + for (const line of lines) { |
| 185 | + if (!line.trim()) continue; |
| 186 | + try { |
| 187 | + const entry = JSON.parse(line) as LogEntry; |
| 188 | + console.log(formatEntryLine(entry)); |
| 189 | + } catch { |
| 190 | + // skip malformed lines during active writing |
| 191 | + } |
182 | 192 | } |
| 193 | + lastSize = s.size; |
| 194 | + } finally { |
| 195 | + await fd.close(); |
183 | 196 | } |
184 | | - lastLineCount = lines.length; |
185 | | - lastSize = s.size; |
186 | 197 | } catch { |
187 | 198 | // ignore read errors during active writing |
188 | 199 | } |
189 | 200 | }); |
190 | 201 |
|
191 | | - // Keep alive until Ctrl+C |
192 | | - process.once("SIGINT", () => { |
| 202 | + // Keep alive until terminated |
| 203 | + const cleanup = () => { |
193 | 204 | watcher.close(); |
194 | 205 | process.exit(0); |
195 | | - }); |
| 206 | + }; |
| 207 | + process.once("SIGINT", cleanup); |
| 208 | + process.once("SIGTERM", cleanup); |
196 | 209 |
|
197 | 210 | await new Promise(() => {}); // block forever |
198 | 211 | } |
@@ -234,7 +247,8 @@ export async function filterSessions(options: { |
234 | 247 | let entries = await readLogEntries(file); |
235 | 248 |
|
236 | 249 | if (options.tool) { |
237 | | - entries = entries.filter((e) => e.tool_name === options.tool || e.method.includes(options.tool!)); |
| 250 | + const toolFilter = options.tool; |
| 251 | + entries = entries.filter((e) => e.tool_name === toolFilter || e.method.includes(toolFilter)); |
238 | 252 | } |
239 | 253 | if (options.errors) { |
240 | 254 | entries = entries.filter((e) => e.error); |
|
0 commit comments