@owloops/claude-powerline 1.24.4 → 1.25.1
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/browser.d.ts +676 -0
- package/dist/browser.js +3 -0
- package/dist/index.mjs +10 -10
- package/package.json +9 -1
- package/plugin/templates/config-tui-compact.json +4 -4
- package/plugin/templates/config-tui-full.json +5 -5
- package/plugin/templates/config-tui-standard.json +5 -5
- package/src/browser.ts +203 -0
- package/src/config/defaults.ts +79 -0
- package/src/config/loader.ts +462 -0
- package/src/index.ts +90 -0
- package/src/powerline.ts +904 -0
- package/src/segments/block.ts +31 -0
- package/src/segments/context.ts +221 -0
- package/src/segments/git.ts +492 -0
- package/src/segments/index.ts +25 -0
- package/src/segments/metrics.ts +175 -0
- package/src/segments/pricing.ts +454 -0
- package/src/segments/renderer.ts +796 -0
- package/src/segments/session.ts +207 -0
- package/src/segments/tmux.ts +35 -0
- package/src/segments/today.ts +191 -0
- package/src/themes/dark.ts +52 -0
- package/src/themes/gruvbox.ts +52 -0
- package/src/themes/index.ts +131 -0
- package/src/themes/light.ts +52 -0
- package/src/themes/nord.ts +52 -0
- package/src/themes/rose-pine.ts +52 -0
- package/src/themes/tokyo-night.ts +52 -0
- package/src/tui/grid.ts +712 -0
- package/src/tui/index.ts +4 -0
- package/src/tui/layouts.ts +285 -0
- package/src/tui/primitives.ts +175 -0
- package/src/tui/renderer.ts +206 -0
- package/src/tui/sections.ts +1080 -0
- package/src/tui/types.ts +181 -0
- package/src/utils/budget.ts +47 -0
- package/src/utils/cache.ts +247 -0
- package/src/utils/claude.ts +489 -0
- package/src/utils/color-support.ts +118 -0
- package/src/utils/colors.ts +120 -0
- package/src/utils/constants.ts +176 -0
- package/src/utils/formatters.ts +160 -0
- package/src/utils/logger.ts +5 -0
- package/src/utils/terminal-width.ts +117 -0
- package/src/utils/terminal.ts +11 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { existsSync, createReadStream } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
6
|
+
import { debug } from "./logger";
|
|
7
|
+
|
|
8
|
+
export interface ClaudeHookData {
|
|
9
|
+
hook_event_name: string;
|
|
10
|
+
session_id: string;
|
|
11
|
+
transcript_path: string;
|
|
12
|
+
cwd: string;
|
|
13
|
+
model: {
|
|
14
|
+
id: string;
|
|
15
|
+
display_name: string;
|
|
16
|
+
};
|
|
17
|
+
workspace: {
|
|
18
|
+
current_dir: string;
|
|
19
|
+
project_dir: string;
|
|
20
|
+
};
|
|
21
|
+
version?: string;
|
|
22
|
+
output_style?: {
|
|
23
|
+
name: string;
|
|
24
|
+
};
|
|
25
|
+
cost?: {
|
|
26
|
+
total_cost_usd: number;
|
|
27
|
+
total_duration_ms: number;
|
|
28
|
+
total_api_duration_ms: number;
|
|
29
|
+
total_lines_added: number;
|
|
30
|
+
total_lines_removed: number;
|
|
31
|
+
};
|
|
32
|
+
context_window?: {
|
|
33
|
+
total_input_tokens: number;
|
|
34
|
+
total_output_tokens: number;
|
|
35
|
+
context_window_size: number;
|
|
36
|
+
used_percentage?: number | null;
|
|
37
|
+
remaining_percentage?: number | null;
|
|
38
|
+
current_usage?: {
|
|
39
|
+
input_tokens: number;
|
|
40
|
+
output_tokens: number;
|
|
41
|
+
cache_creation_input_tokens: number;
|
|
42
|
+
cache_read_input_tokens: number;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
exceeds_200k_tokens?: boolean;
|
|
46
|
+
rate_limits?: {
|
|
47
|
+
five_hour?: {
|
|
48
|
+
used_percentage: number;
|
|
49
|
+
resets_at: number;
|
|
50
|
+
};
|
|
51
|
+
seven_day?: {
|
|
52
|
+
used_percentage: number;
|
|
53
|
+
resets_at: number;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getClaudePaths(): string[] {
|
|
59
|
+
const paths: string[] = [];
|
|
60
|
+
|
|
61
|
+
const envPath = process.env.CLAUDE_CONFIG_DIR;
|
|
62
|
+
if (envPath) {
|
|
63
|
+
envPath.split(",").forEach((path) => {
|
|
64
|
+
const trimmedPath = path.trim();
|
|
65
|
+
if (existsSync(trimmedPath)) {
|
|
66
|
+
paths.push(trimmedPath);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (paths.length === 0) {
|
|
72
|
+
const homeDir = homedir();
|
|
73
|
+
const configPath = join(homeDir, ".config", "claude");
|
|
74
|
+
const claudePath = join(homeDir, ".claude");
|
|
75
|
+
|
|
76
|
+
if (existsSync(configPath)) {
|
|
77
|
+
paths.push(configPath);
|
|
78
|
+
}
|
|
79
|
+
if (existsSync(claudePath)) {
|
|
80
|
+
paths.push(claudePath);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return paths;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function findProjectPaths(
|
|
88
|
+
claudePaths: string[],
|
|
89
|
+
): Promise<string[]> {
|
|
90
|
+
const projectPaths: string[] = [];
|
|
91
|
+
|
|
92
|
+
for (const claudePath of claudePaths) {
|
|
93
|
+
const projectsDir = join(claudePath, "projects");
|
|
94
|
+
|
|
95
|
+
if (existsSync(projectsDir)) {
|
|
96
|
+
try {
|
|
97
|
+
const entries = await readdir(projectsDir, { withFileTypes: true });
|
|
98
|
+
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (entry.isDirectory()) {
|
|
101
|
+
const projectPath = join(projectsDir, entry.name);
|
|
102
|
+
projectPaths.push(projectPath);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
debug(`Failed to read projects directory ${projectsDir}:`, error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return projectPaths;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function findTranscriptFile(
|
|
115
|
+
sessionId: string,
|
|
116
|
+
): Promise<string | null> {
|
|
117
|
+
const claudePaths = getClaudePaths();
|
|
118
|
+
const projectPaths = await findProjectPaths(claudePaths);
|
|
119
|
+
|
|
120
|
+
for (const projectPath of projectPaths) {
|
|
121
|
+
const transcriptPath = join(projectPath, `${sessionId}.jsonl`);
|
|
122
|
+
if (existsSync(transcriptPath)) {
|
|
123
|
+
return transcriptPath;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function findAgentTranscripts(
|
|
131
|
+
sessionId: string,
|
|
132
|
+
projectPath: string,
|
|
133
|
+
): Promise<string[]> {
|
|
134
|
+
const agentFiles: string[] = [];
|
|
135
|
+
|
|
136
|
+
const subagentsDir = join(projectPath, sessionId, "subagents");
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const files = await readdir(subagentsDir);
|
|
140
|
+
const agentFileNames = files.filter(
|
|
141
|
+
(f) => f.startsWith("agent-") && f.endsWith(".jsonl"),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
for (const fileName of agentFileNames) {
|
|
145
|
+
const filePath = join(subagentsDir, fileName);
|
|
146
|
+
try {
|
|
147
|
+
const content = await readFile(filePath, "utf-8");
|
|
148
|
+
const firstLine = content.split("\n")[0];
|
|
149
|
+
if (firstLine) {
|
|
150
|
+
const parsed = JSON.parse(firstLine);
|
|
151
|
+
if (parsed.sessionId === sessionId) {
|
|
152
|
+
agentFiles.push(filePath);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
debug(`Failed to check agent file ${filePath}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
debug(`Failed to read subagents directory ${subagentsDir}:`, error);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return agentFiles;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function getEarliestTimestamp(
|
|
167
|
+
filePath: string,
|
|
168
|
+
): Promise<Date | null> {
|
|
169
|
+
try {
|
|
170
|
+
const content = await readFile(filePath, "utf-8");
|
|
171
|
+
const lines = content.trim().split("\n");
|
|
172
|
+
|
|
173
|
+
let earliestDate: Date | null = null;
|
|
174
|
+
for (const line of lines) {
|
|
175
|
+
if (!line.trim()) continue;
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const json = JSON.parse(line);
|
|
179
|
+
if (json.timestamp && typeof json.timestamp === "string") {
|
|
180
|
+
const date = new Date(json.timestamp);
|
|
181
|
+
if (!isNaN(date.getTime())) {
|
|
182
|
+
if (earliestDate === null || date < earliestDate) {
|
|
183
|
+
earliestDate = date;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return earliestDate;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
debug(`Failed to get earliest timestamp for ${filePath}:`, error);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function sortFilesByTimestamp(
|
|
199
|
+
files: string[],
|
|
200
|
+
oldestFirst = true,
|
|
201
|
+
): Promise<string[]> {
|
|
202
|
+
const filesWithTimestamps = await Promise.all(
|
|
203
|
+
files.map(async (file) => ({
|
|
204
|
+
file,
|
|
205
|
+
timestamp: await getEarliestTimestamp(file),
|
|
206
|
+
})),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
return filesWithTimestamps
|
|
210
|
+
.sort((a, b) => {
|
|
211
|
+
if (a.timestamp === null && b.timestamp === null) return 0;
|
|
212
|
+
if (a.timestamp === null) return 1;
|
|
213
|
+
if (b.timestamp === null) return -1;
|
|
214
|
+
const sortOrder = oldestFirst ? 1 : -1;
|
|
215
|
+
return sortOrder * (a.timestamp.getTime() - b.timestamp.getTime());
|
|
216
|
+
})
|
|
217
|
+
.map((item) => item.file);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function getFileModificationDate(
|
|
221
|
+
filePath: string,
|
|
222
|
+
): Promise<Date | null> {
|
|
223
|
+
try {
|
|
224
|
+
const stats = await stat(filePath);
|
|
225
|
+
return stats.mtime;
|
|
226
|
+
} catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface ParsedEntry {
|
|
232
|
+
timestamp: Date;
|
|
233
|
+
message?: {
|
|
234
|
+
id?: string;
|
|
235
|
+
usage?: {
|
|
236
|
+
input_tokens?: number;
|
|
237
|
+
output_tokens?: number;
|
|
238
|
+
cache_creation_input_tokens?: number;
|
|
239
|
+
cache_read_input_tokens?: number;
|
|
240
|
+
};
|
|
241
|
+
model?: string;
|
|
242
|
+
};
|
|
243
|
+
costUSD?: number;
|
|
244
|
+
isSidechain?: boolean;
|
|
245
|
+
raw: Record<string, unknown>;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function createUniqueHash(entry: ParsedEntry): string | null {
|
|
249
|
+
const messageId =
|
|
250
|
+
entry.message?.id ||
|
|
251
|
+
(typeof entry.raw.message === "object" &&
|
|
252
|
+
entry.raw.message !== null &&
|
|
253
|
+
"id" in entry.raw.message
|
|
254
|
+
? (entry.raw.message.id as string)
|
|
255
|
+
: undefined);
|
|
256
|
+
const requestId =
|
|
257
|
+
"requestId" in entry.raw ? (entry.raw.requestId as string) : undefined;
|
|
258
|
+
|
|
259
|
+
if (!messageId || !requestId) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return `${messageId}:${requestId}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const STREAMING_THRESHOLD_BYTES = 1024 * 1024;
|
|
267
|
+
|
|
268
|
+
export async function parseJsonlFile(filePath: string): Promise<ParsedEntry[]> {
|
|
269
|
+
try {
|
|
270
|
+
const stats = await stat(filePath);
|
|
271
|
+
const fileSizeBytes = stats.size;
|
|
272
|
+
let entries: ParsedEntry[];
|
|
273
|
+
|
|
274
|
+
if (fileSizeBytes > STREAMING_THRESHOLD_BYTES) {
|
|
275
|
+
debug(
|
|
276
|
+
`Using streaming parser for large file ${filePath} (${Math.round(fileSizeBytes / 1024)}KB)`,
|
|
277
|
+
);
|
|
278
|
+
entries = await parseJsonlFileStreaming(filePath);
|
|
279
|
+
} else {
|
|
280
|
+
entries = await parseJsonlFileInMemory(filePath);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
debug(`Parsed ${entries.length} entries from ${filePath}`);
|
|
284
|
+
|
|
285
|
+
return entries;
|
|
286
|
+
} catch (error) {
|
|
287
|
+
debug(`Failed to read file ${filePath}:`, error);
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function parseJsonlFileInMemory(
|
|
293
|
+
filePath: string,
|
|
294
|
+
): Promise<ParsedEntry[]> {
|
|
295
|
+
const content = await readFile(filePath, "utf-8");
|
|
296
|
+
const lines = content
|
|
297
|
+
.trim()
|
|
298
|
+
.split("\n")
|
|
299
|
+
.filter((line) => line.trim());
|
|
300
|
+
const entries: ParsedEntry[] = [];
|
|
301
|
+
|
|
302
|
+
for (const line of lines) {
|
|
303
|
+
try {
|
|
304
|
+
const raw = JSON.parse(line);
|
|
305
|
+
if (!raw.timestamp) continue;
|
|
306
|
+
|
|
307
|
+
const entry: ParsedEntry = {
|
|
308
|
+
timestamp: new Date(raw.timestamp),
|
|
309
|
+
message: raw.message,
|
|
310
|
+
costUSD: typeof raw.costUSD === "number" ? raw.costUSD : undefined,
|
|
311
|
+
isSidechain: raw.isSidechain === true,
|
|
312
|
+
raw,
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
entries.push(entry);
|
|
316
|
+
} catch (parseError) {
|
|
317
|
+
debug(`Failed to parse JSONL line: ${parseError}`);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return entries;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function parseJsonlFileStreaming(
|
|
326
|
+
filePath: string,
|
|
327
|
+
): Promise<ParsedEntry[]> {
|
|
328
|
+
return new Promise((resolve, reject) => {
|
|
329
|
+
const entries: ParsedEntry[] = [];
|
|
330
|
+
const fileStream = createReadStream(filePath, { encoding: "utf8" });
|
|
331
|
+
const rl = createInterface({
|
|
332
|
+
input: fileStream,
|
|
333
|
+
crlfDelay: Infinity,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
rl.on("line", (line) => {
|
|
337
|
+
const trimmedLine = line.trim();
|
|
338
|
+
if (!trimmedLine) return;
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
const raw = JSON.parse(trimmedLine);
|
|
342
|
+
if (!raw.timestamp) return;
|
|
343
|
+
|
|
344
|
+
const entry: ParsedEntry = {
|
|
345
|
+
timestamp: new Date(raw.timestamp),
|
|
346
|
+
message: raw.message,
|
|
347
|
+
costUSD: typeof raw.costUSD === "number" ? raw.costUSD : undefined,
|
|
348
|
+
isSidechain: raw.isSidechain === true,
|
|
349
|
+
raw,
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
entries.push(entry);
|
|
353
|
+
} catch (parseError) {
|
|
354
|
+
debug(`Failed to parse JSONL line: ${parseError}`);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
rl.on("close", () => {
|
|
359
|
+
resolve(entries);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
rl.on("error", (error) => {
|
|
363
|
+
debug(`Streaming parser error for ${filePath}:`, error);
|
|
364
|
+
reject(error);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
fileStream.on("error", (error) => {
|
|
368
|
+
debug(`File stream error for ${filePath}:`, error);
|
|
369
|
+
reject(error);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
interface FileStat {
|
|
375
|
+
filePath: string;
|
|
376
|
+
mtime: Date;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function statFile(filePath: string): Promise<FileStat | null> {
|
|
380
|
+
try {
|
|
381
|
+
const mtime = await getFileModificationDate(filePath);
|
|
382
|
+
return mtime ? { filePath, mtime } : null;
|
|
383
|
+
} catch {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function collectProjectFiles(
|
|
389
|
+
projectPath: string,
|
|
390
|
+
fileFilter?: (filePath: string, modTime: Date) => boolean,
|
|
391
|
+
): Promise<FileStat[]> {
|
|
392
|
+
try {
|
|
393
|
+
const entries = await readdir(projectPath, { withFileTypes: true });
|
|
394
|
+
|
|
395
|
+
const sessionFiles = entries
|
|
396
|
+
.filter((e) => !e.isDirectory() && e.name.endsWith(".jsonl"))
|
|
397
|
+
.map((e) => statFile(join(projectPath, e.name)));
|
|
398
|
+
|
|
399
|
+
const subagentFiles = entries
|
|
400
|
+
.filter((e) => e.isDirectory())
|
|
401
|
+
.map(async (e) => {
|
|
402
|
+
const subagentsDir = join(projectPath, e.name, "subagents");
|
|
403
|
+
try {
|
|
404
|
+
const files = await readdir(subagentsDir);
|
|
405
|
+
return files
|
|
406
|
+
.filter((f) => f.startsWith("agent-") && f.endsWith(".jsonl"))
|
|
407
|
+
.map((f) => statFile(join(subagentsDir, f)));
|
|
408
|
+
} catch {
|
|
409
|
+
return [];
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const [sessionResults, subagentResults] = await Promise.all([
|
|
414
|
+
Promise.all(sessionFiles),
|
|
415
|
+
Promise.all(subagentFiles).then((nested) => Promise.all(nested.flat())),
|
|
416
|
+
]);
|
|
417
|
+
|
|
418
|
+
return [...sessionResults, ...subagentResults].filter(
|
|
419
|
+
(s): s is FileStat =>
|
|
420
|
+
s !== null && (!fileFilter || fileFilter(s.filePath, s.mtime)),
|
|
421
|
+
);
|
|
422
|
+
} catch (dirError) {
|
|
423
|
+
debug(`Failed to read project directory ${projectPath}:`, dirError);
|
|
424
|
+
return [];
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Loads entries from Claude projects with deterministic deduplication.
|
|
430
|
+
* @param timeFilter Optional filter to apply based on timestamp
|
|
431
|
+
* @param fileFilter Optional filter to apply based on file path and modification time
|
|
432
|
+
* @param sortFiles Whether to sort files by modification time
|
|
433
|
+
* @returns Deduplicated entries sorted by timestamp
|
|
434
|
+
* @note Sorts entries by timestamp before deduplication to ensure consistent
|
|
435
|
+
* duplicate selection. Otherwise, parallel file loading causes race conditions
|
|
436
|
+
* where different duplicates are kept on each run, leading to flickering values.
|
|
437
|
+
*/
|
|
438
|
+
export async function loadEntriesFromProjects(
|
|
439
|
+
timeFilter?: (entry: ParsedEntry) => boolean,
|
|
440
|
+
fileFilter?: (filePath: string, modTime: Date) => boolean,
|
|
441
|
+
sortFiles = false,
|
|
442
|
+
): Promise<ParsedEntry[]> {
|
|
443
|
+
const claudePaths = getClaudePaths();
|
|
444
|
+
const projectPaths = await findProjectPaths(claudePaths);
|
|
445
|
+
const processedHashes = new Set<string>();
|
|
446
|
+
|
|
447
|
+
const allFilesPromises = projectPaths.map((projectPath) =>
|
|
448
|
+
collectProjectFiles(projectPath, fileFilter),
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
const allFileResults = await Promise.all(allFilesPromises);
|
|
452
|
+
const allFilesWithMtime = allFileResults
|
|
453
|
+
.flat()
|
|
454
|
+
.filter((file): file is { filePath: string; mtime: Date } => file !== null);
|
|
455
|
+
|
|
456
|
+
if (sortFiles) {
|
|
457
|
+
allFilesWithMtime.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const allFiles = allFilesWithMtime.map((file) => file.filePath);
|
|
461
|
+
|
|
462
|
+
const entries: ParsedEntry[] = [];
|
|
463
|
+
|
|
464
|
+
const filePromises = allFiles.map(async (filePath) => {
|
|
465
|
+
const fileEntries = await parseJsonlFile(filePath);
|
|
466
|
+
return fileEntries.filter((entry) => !timeFilter || timeFilter(entry));
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const fileResults = await Promise.all(filePromises);
|
|
470
|
+
for (const fileEntries of fileResults) {
|
|
471
|
+
entries.push(...fileEntries);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
entries.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
475
|
+
|
|
476
|
+
const deduplicatedEntries: ParsedEntry[] = [];
|
|
477
|
+
for (const entry of entries) {
|
|
478
|
+
const uniqueHash = createUniqueHash(entry);
|
|
479
|
+
if (uniqueHash && processedHashes.has(uniqueHash)) {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (uniqueHash) {
|
|
483
|
+
processedHashes.add(uniqueHash);
|
|
484
|
+
}
|
|
485
|
+
deduplicatedEntries.push(entry);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return deduplicatedEntries;
|
|
489
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import tty from "node:tty";
|
|
3
|
+
|
|
4
|
+
export function getColorSupport(): "none" | "ansi" | "ansi256" | "truecolor" {
|
|
5
|
+
const { env } = process;
|
|
6
|
+
|
|
7
|
+
let colorEnabled = true;
|
|
8
|
+
|
|
9
|
+
if (env.NO_COLOR && env.NO_COLOR !== "") {
|
|
10
|
+
colorEnabled = false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const forceColor = env.FORCE_COLOR;
|
|
14
|
+
if (forceColor && forceColor !== "") {
|
|
15
|
+
if (forceColor === "false" || forceColor === "0") {
|
|
16
|
+
return "none";
|
|
17
|
+
}
|
|
18
|
+
if (forceColor === "true" || forceColor === "1") {
|
|
19
|
+
return "ansi";
|
|
20
|
+
}
|
|
21
|
+
if (forceColor === "2") {
|
|
22
|
+
return "ansi256";
|
|
23
|
+
}
|
|
24
|
+
if (forceColor === "3") {
|
|
25
|
+
return "truecolor";
|
|
26
|
+
}
|
|
27
|
+
return "ansi";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!colorEnabled) {
|
|
31
|
+
return "none";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (env.TERM === "dumb") {
|
|
35
|
+
return "none";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (env.CI) {
|
|
39
|
+
if (
|
|
40
|
+
["GITHUB_ACTIONS", "GITEA_ACTIONS", "CIRCLECI"].some((key) => key in env)
|
|
41
|
+
) {
|
|
42
|
+
return "truecolor";
|
|
43
|
+
}
|
|
44
|
+
return "ansi";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (env.COLORTERM === "truecolor") {
|
|
48
|
+
return "truecolor";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const truecolorTerminals = [
|
|
52
|
+
"xterm-kitty",
|
|
53
|
+
"xterm-ghostty",
|
|
54
|
+
"wezterm",
|
|
55
|
+
"alacritty",
|
|
56
|
+
"foot",
|
|
57
|
+
"contour",
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
if (truecolorTerminals.includes(env.TERM || "")) {
|
|
61
|
+
return "truecolor";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (env.TERM_PROGRAM) {
|
|
65
|
+
switch (env.TERM_PROGRAM) {
|
|
66
|
+
case "iTerm.app":
|
|
67
|
+
return "truecolor";
|
|
68
|
+
case "Apple_Terminal":
|
|
69
|
+
return "ansi256";
|
|
70
|
+
case "vscode":
|
|
71
|
+
return "truecolor";
|
|
72
|
+
case "Tabby":
|
|
73
|
+
return "truecolor";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (/-256(color)?$/i.test(env.TERM || "")) {
|
|
78
|
+
return "ansi256";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (/-truecolor$/i.test(env.TERM || "")) {
|
|
82
|
+
return "truecolor";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (
|
|
86
|
+
/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(
|
|
87
|
+
env.TERM || "",
|
|
88
|
+
)
|
|
89
|
+
) {
|
|
90
|
+
return "ansi";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (env.COLORTERM) {
|
|
94
|
+
return "ansi";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (tty?.WriteStream?.prototype?.hasColors) {
|
|
98
|
+
try {
|
|
99
|
+
const colors = tty.WriteStream.prototype.hasColors();
|
|
100
|
+
if (!colors) {
|
|
101
|
+
return "none";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const has256Colors = tty.WriteStream.prototype.hasColors(256);
|
|
105
|
+
const has16mColors = tty.WriteStream.prototype.hasColors(16777216);
|
|
106
|
+
|
|
107
|
+
if (has16mColors) {
|
|
108
|
+
return "truecolor";
|
|
109
|
+
} else if (has256Colors) {
|
|
110
|
+
return "ansi256";
|
|
111
|
+
} else {
|
|
112
|
+
return "ansi";
|
|
113
|
+
}
|
|
114
|
+
} catch {}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return "ansi";
|
|
118
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export function hexToAnsi(hex: string, isBackground: boolean): string {
|
|
2
|
+
if (
|
|
3
|
+
isBackground &&
|
|
4
|
+
(hex.toLowerCase() === "transparent" || hex.toLowerCase() === "none")
|
|
5
|
+
) {
|
|
6
|
+
return "\x1b[49m";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
10
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
11
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
12
|
+
return `\x1b[${isBackground ? "48" : "38"};2;${r};${g};${b}m`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function extractBgToFg(
|
|
16
|
+
ansiCode: string,
|
|
17
|
+
useTextOnly: boolean = false,
|
|
18
|
+
): string {
|
|
19
|
+
if (!ansiCode || ansiCode === "") {
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const truecolorMatch = ansiCode.match(/48;2;(\d+);(\d+);(\d+)/);
|
|
24
|
+
if (truecolorMatch) {
|
|
25
|
+
return `\x1b[38;2;${truecolorMatch[1]};${truecolorMatch[2]};${truecolorMatch[3]}m`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (useTextOnly) {
|
|
29
|
+
return "\x1b[37m";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (ansiCode.includes("\x1b[") && ansiCode.includes("m")) {
|
|
33
|
+
const codeMatch = ansiCode.match(/\[(\d+)m/);
|
|
34
|
+
if (codeMatch && codeMatch[1]) {
|
|
35
|
+
const bgCode = parseInt(codeMatch[1], 10);
|
|
36
|
+
if (bgCode >= 40 && bgCode <= 47) {
|
|
37
|
+
const fgCode = bgCode - 10;
|
|
38
|
+
return `\x1b[${fgCode}m`;
|
|
39
|
+
}
|
|
40
|
+
if (bgCode >= 100 && bgCode <= 107) {
|
|
41
|
+
const fgCode = bgCode - 10;
|
|
42
|
+
return `\x1b[${fgCode}m`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return ansiCode.replace("48", "38");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function hexTo256Ansi(hex: string, isBackground: boolean): string {
|
|
51
|
+
if (
|
|
52
|
+
isBackground &&
|
|
53
|
+
(hex.toLowerCase() === "transparent" || hex.toLowerCase() === "none")
|
|
54
|
+
) {
|
|
55
|
+
return "\x1b[49m";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
59
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
60
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
61
|
+
|
|
62
|
+
const toAnsi256 = (r: number, g: number, b: number): number => {
|
|
63
|
+
if (r === g && g === b) {
|
|
64
|
+
if (r < 8) return 16;
|
|
65
|
+
if (r > 248) return 231;
|
|
66
|
+
return Math.round(((r - 8) / 247) * 24) + 232;
|
|
67
|
+
}
|
|
68
|
+
return (
|
|
69
|
+
16 +
|
|
70
|
+
36 * Math.round((r / 255) * 5) +
|
|
71
|
+
6 * Math.round((g / 255) * 5) +
|
|
72
|
+
Math.round((b / 255) * 5)
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const colorCode = toAnsi256(r, g, b);
|
|
77
|
+
return `\x1b[${isBackground ? "48" : "38"};5;${colorCode}m`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function hexToBasicAnsi(hex: string, isBackground: boolean): string {
|
|
81
|
+
if (
|
|
82
|
+
isBackground &&
|
|
83
|
+
(hex.toLowerCase() === "transparent" || hex.toLowerCase() === "none")
|
|
84
|
+
) {
|
|
85
|
+
return "\x1b[49m";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (isBackground) {
|
|
89
|
+
return "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
93
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
94
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
95
|
+
|
|
96
|
+
if (g > r && g > b && g > 120) {
|
|
97
|
+
return "\x1b[32m";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (r > g && r > b && r > 120) {
|
|
101
|
+
return "\x1b[31m";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (b > r && b > g && b > 120) {
|
|
105
|
+
return "\x1b[34m";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const brightness = (r + g + b) / 3;
|
|
109
|
+
return brightness > 150 ? "\x1b[37m" : "\x1b[90m";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function hexColorDistance(hex1: string, hex2: string): number {
|
|
113
|
+
const r1 = parseInt(hex1.slice(1, 3), 16);
|
|
114
|
+
const g1 = parseInt(hex1.slice(3, 5), 16);
|
|
115
|
+
const b1 = parseInt(hex1.slice(5, 7), 16);
|
|
116
|
+
const r2 = parseInt(hex2.slice(1, 3), 16);
|
|
117
|
+
const g2 = parseInt(hex2.slice(3, 5), 16);
|
|
118
|
+
const b2 = parseInt(hex2.slice(5, 7), 16);
|
|
119
|
+
return Math.sqrt((r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2);
|
|
120
|
+
}
|