@owloops/claude-powerline 1.24.3 → 1.25.0

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.
Files changed (48) hide show
  1. package/README.md +5 -43
  2. package/dist/browser.d.ts +676 -0
  3. package/dist/browser.js +3 -0
  4. package/dist/index.mjs +12 -12
  5. package/package.json +9 -1
  6. package/plugin/templates/config-full.json +1 -1
  7. package/plugin/templates/config-tui-compact.json +3 -3
  8. package/plugin/templates/config-tui-full.json +4 -4
  9. package/plugin/templates/config-tui-standard.json +4 -4
  10. package/src/browser.ts +203 -0
  11. package/src/config/defaults.ts +79 -0
  12. package/src/config/loader.ts +462 -0
  13. package/src/index.ts +90 -0
  14. package/src/powerline.ts +904 -0
  15. package/src/segments/block.ts +31 -0
  16. package/src/segments/context.ts +221 -0
  17. package/src/segments/git.ts +492 -0
  18. package/src/segments/index.ts +25 -0
  19. package/src/segments/metrics.ts +175 -0
  20. package/src/segments/pricing.ts +454 -0
  21. package/src/segments/renderer.ts +796 -0
  22. package/src/segments/session.ts +207 -0
  23. package/src/segments/tmux.ts +35 -0
  24. package/src/segments/today.ts +191 -0
  25. package/src/themes/dark.ts +52 -0
  26. package/src/themes/gruvbox.ts +52 -0
  27. package/src/themes/index.ts +131 -0
  28. package/src/themes/light.ts +52 -0
  29. package/src/themes/nord.ts +52 -0
  30. package/src/themes/rose-pine.ts +52 -0
  31. package/src/themes/tokyo-night.ts +52 -0
  32. package/src/tui/grid.ts +712 -0
  33. package/src/tui/index.ts +4 -0
  34. package/src/tui/layouts.ts +285 -0
  35. package/src/tui/primitives.ts +175 -0
  36. package/src/tui/renderer.ts +206 -0
  37. package/src/tui/sections.ts +1080 -0
  38. package/src/tui/types.ts +181 -0
  39. package/src/utils/budget.ts +47 -0
  40. package/src/utils/cache.ts +247 -0
  41. package/src/utils/claude.ts +489 -0
  42. package/src/utils/color-support.ts +118 -0
  43. package/src/utils/colors.ts +120 -0
  44. package/src/utils/constants.ts +176 -0
  45. package/src/utils/formatters.ts +160 -0
  46. package/src/utils/logger.ts +5 -0
  47. package/src/utils/terminal-width.ts +117 -0
  48. package/src/utils/terminal.ts +11 -0
@@ -0,0 +1,492 @@
1
+ import { exec } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { debug } from "../utils/logger";
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ export interface GitInfo {
10
+ branch: string;
11
+ status: "clean" | "dirty" | "conflicts";
12
+ ahead: number;
13
+ behind: number;
14
+ sha?: string;
15
+ staged?: number;
16
+ unstaged?: number;
17
+ untracked?: number;
18
+ conflicts?: number;
19
+ operation?: string;
20
+ tag?: string;
21
+ timeSinceCommit?: number;
22
+ stashCount?: number;
23
+ upstream?: string;
24
+ repoName?: string;
25
+ isWorktree?: boolean;
26
+ }
27
+
28
+ export class GitService {
29
+ private isGitRepo(workingDir: string): boolean {
30
+ try {
31
+ return fs.existsSync(path.join(workingDir, ".git"));
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ private async execGitAsync(
38
+ command: string,
39
+ options: { cwd: string; encoding: string; timeout: number },
40
+ ): Promise<{ stdout: string }> {
41
+ return execAsync(command, {
42
+ ...options,
43
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
44
+ });
45
+ }
46
+
47
+ private async findGitRoot(workingDir: string): Promise<string | null> {
48
+ try {
49
+ const result = await this.execGitAsync("git rev-parse --show-toplevel", {
50
+ cwd: workingDir,
51
+ encoding: "utf8",
52
+ timeout: 2000,
53
+ });
54
+ const gitRoot = result.stdout.trim();
55
+ return gitRoot || null;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ async getGitInfo(
62
+ workingDir: string,
63
+ options: {
64
+ showSha?: boolean;
65
+ showWorkingTree?: boolean;
66
+ showOperation?: boolean;
67
+ showTag?: boolean;
68
+ showTimeSinceCommit?: boolean;
69
+ showStashCount?: boolean;
70
+ showUpstream?: boolean;
71
+ showRepoName?: boolean;
72
+ } = {},
73
+ projectDir?: string,
74
+ ): Promise<GitInfo | null> {
75
+ let gitDir: string;
76
+ const isWorktreeDir = this.isWorktree(workingDir);
77
+
78
+ if (isWorktreeDir) {
79
+ // Worktree's .git is a file pointing to the main repo;
80
+ // git commands must run from the worktree directory.
81
+ gitDir = workingDir;
82
+ } else if (projectDir && this.isGitRepo(projectDir)) {
83
+ gitDir = projectDir;
84
+ } else if (this.isGitRepo(workingDir)) {
85
+ gitDir = workingDir;
86
+ } else {
87
+ const foundGitRoot = await this.findGitRoot(workingDir);
88
+ if (!foundGitRoot) {
89
+ return null;
90
+ }
91
+ gitDir = foundGitRoot;
92
+ }
93
+
94
+ try {
95
+ const statusWithBranch = await this.getStatusWithBranchAsync(gitDir);
96
+ const aheadBehind = await this.getAheadBehindAsync(gitDir);
97
+
98
+ const result: GitInfo = {
99
+ branch: statusWithBranch.branch || "detached",
100
+ status: statusWithBranch.status,
101
+ ahead: aheadBehind.ahead,
102
+ behind: aheadBehind.behind,
103
+ };
104
+
105
+ if (options.showWorkingTree && statusWithBranch.workingTree) {
106
+ result.staged = statusWithBranch.workingTree.staged;
107
+ result.unstaged = statusWithBranch.workingTree.unstaged;
108
+ result.untracked = statusWithBranch.workingTree.untracked;
109
+ result.conflicts = statusWithBranch.workingTree.conflicts;
110
+ }
111
+
112
+ const heavyOperations: Record<string, Promise<unknown>> = {};
113
+ const lightOperations: Record<string, Promise<unknown>> = {};
114
+
115
+ if (options.showSha) {
116
+ heavyOperations.sha = this.getShaAsync(gitDir);
117
+ }
118
+
119
+ if (options.showTag) {
120
+ heavyOperations.tag = this.getNearestTagAsync(gitDir);
121
+ }
122
+
123
+ if (options.showTimeSinceCommit) {
124
+ heavyOperations.timeSinceCommit =
125
+ this.getTimeSinceLastCommitAsync(gitDir);
126
+ }
127
+
128
+ if (options.showStashCount) {
129
+ lightOperations.stashCount = this.getStashCountAsync(gitDir);
130
+ }
131
+
132
+ if (options.showUpstream) {
133
+ lightOperations.upstream = this.getUpstreamAsync(gitDir);
134
+ }
135
+
136
+ if (options.showRepoName) {
137
+ lightOperations.repoName = this.getRepoNameAsync(gitDir);
138
+ }
139
+
140
+ const resultMap = new Map<string, unknown>();
141
+
142
+ for (const [key, promise] of Object.entries(heavyOperations)) {
143
+ try {
144
+ const value = await promise;
145
+ resultMap.set(key, value);
146
+ } catch {}
147
+ }
148
+
149
+ if (Object.keys(lightOperations).length > 0) {
150
+ const lightResults = await Promise.allSettled(
151
+ Object.entries(lightOperations).map(async ([key, promise]) => ({
152
+ key,
153
+ value: await promise,
154
+ })),
155
+ );
156
+
157
+ lightResults.forEach((result) => {
158
+ if (result.status === "fulfilled") {
159
+ resultMap.set(result.value.key, result.value.value);
160
+ }
161
+ });
162
+ }
163
+
164
+ if (options.showSha) {
165
+ result.sha = (resultMap.get("sha") as string) || undefined;
166
+ }
167
+
168
+ if (options.showOperation) {
169
+ result.operation = this.getOngoingOperation(gitDir) || undefined;
170
+ }
171
+
172
+ if (options.showTag) {
173
+ result.tag = (resultMap.get("tag") as string) || undefined;
174
+ }
175
+
176
+ if (options.showTimeSinceCommit) {
177
+ result.timeSinceCommit =
178
+ (resultMap.get("timeSinceCommit") as number) || undefined;
179
+ }
180
+
181
+ if (options.showStashCount) {
182
+ result.stashCount = (resultMap.get("stashCount") as number) || 0;
183
+ }
184
+
185
+ if (options.showUpstream) {
186
+ result.upstream = (resultMap.get("upstream") as string) || undefined;
187
+ }
188
+
189
+ if (options.showRepoName) {
190
+ result.repoName = (resultMap.get("repoName") as string) || undefined;
191
+ result.isWorktree = isWorktreeDir;
192
+ }
193
+
194
+ return result;
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+
200
+ private async getShaAsync(workingDir: string): Promise<string | null> {
201
+ try {
202
+ const result = await this.execGitAsync("git rev-parse --short=7 HEAD", {
203
+ cwd: workingDir,
204
+ encoding: "utf8",
205
+ timeout: 2000,
206
+ });
207
+ const sha = result.stdout.trim();
208
+
209
+ return sha || null;
210
+ } catch {
211
+ return null;
212
+ }
213
+ }
214
+
215
+ private resolveGitDir(workingDir: string): string {
216
+ const dotGit = path.join(workingDir, ".git");
217
+ if (fs.existsSync(dotGit) && fs.statSync(dotGit).isFile()) {
218
+ const content = fs.readFileSync(dotGit, "utf-8");
219
+ const match = content.match(/^gitdir:\s*(.+)$/m);
220
+ if (match?.[1]) {
221
+ return path.resolve(workingDir, match[1].trim());
222
+ }
223
+ }
224
+ return dotGit;
225
+ }
226
+
227
+ private getOngoingOperation(workingDir: string): string | null {
228
+ try {
229
+ const gitDir = this.resolveGitDir(workingDir);
230
+
231
+ if (fs.existsSync(path.join(gitDir, "MERGE_HEAD"))) return "MERGE";
232
+ if (fs.existsSync(path.join(gitDir, "CHERRY_PICK_HEAD")))
233
+ return "CHERRY-PICK";
234
+ if (fs.existsSync(path.join(gitDir, "REVERT_HEAD"))) return "REVERT";
235
+ if (fs.existsSync(path.join(gitDir, "BISECT_LOG"))) return "BISECT";
236
+ if (
237
+ fs.existsSync(path.join(gitDir, "rebase-merge")) ||
238
+ fs.existsSync(path.join(gitDir, "rebase-apply"))
239
+ )
240
+ return "REBASE";
241
+
242
+ return null;
243
+ } catch {
244
+ return null;
245
+ }
246
+ }
247
+
248
+ private async getNearestTagAsync(workingDir: string): Promise<string | null> {
249
+ try {
250
+ const result = await this.execGitAsync("git describe --tags --abbrev=0", {
251
+ cwd: workingDir,
252
+ encoding: "utf8",
253
+ timeout: 2000,
254
+ });
255
+ const tag = result.stdout.trim();
256
+
257
+ return tag || null;
258
+ } catch {
259
+ return null;
260
+ }
261
+ }
262
+
263
+ private async getTimeSinceLastCommitAsync(
264
+ workingDir: string,
265
+ ): Promise<number | null> {
266
+ try {
267
+ const result = await this.execGitAsync("git log -1 --format=%ct", {
268
+ cwd: workingDir,
269
+ encoding: "utf8",
270
+ timeout: 2000,
271
+ });
272
+ const timestamp = result.stdout.trim();
273
+
274
+ if (!timestamp) return null;
275
+
276
+ const commitTime = parseInt(timestamp) * 1000;
277
+ const now = Date.now();
278
+ return Math.floor((now - commitTime) / 1000);
279
+ } catch {
280
+ return null;
281
+ }
282
+ }
283
+
284
+ private async getStashCountAsync(workingDir: string): Promise<number> {
285
+ try {
286
+ const result = await this.execGitAsync("git stash list", {
287
+ cwd: workingDir,
288
+ encoding: "utf8",
289
+ timeout: 2000,
290
+ });
291
+ const stashList = result.stdout.trim();
292
+
293
+ if (!stashList) return 0;
294
+ return stashList.split("\n").length;
295
+ } catch {
296
+ return 0;
297
+ }
298
+ }
299
+
300
+ private async getUpstreamAsync(workingDir: string): Promise<string | null> {
301
+ try {
302
+ const result = await this.execGitAsync(
303
+ "git rev-parse --abbrev-ref @{u}",
304
+ {
305
+ cwd: workingDir,
306
+ encoding: "utf8",
307
+ timeout: 2000,
308
+ },
309
+ );
310
+ const upstream = result.stdout.trim();
311
+
312
+ return upstream || null;
313
+ } catch {
314
+ return null;
315
+ }
316
+ }
317
+
318
+ private async getRepoNameAsync(workingDir: string): Promise<string | null> {
319
+ try {
320
+ const result = await this.execGitAsync(
321
+ "git config --get remote.origin.url",
322
+ {
323
+ cwd: workingDir,
324
+ encoding: "utf8",
325
+ timeout: 2000,
326
+ },
327
+ );
328
+ const remoteUrl = result.stdout.trim();
329
+
330
+ if (!remoteUrl) return path.basename(workingDir);
331
+
332
+ const match = remoteUrl.match(/\/([^/]+?)(\.git)?$/);
333
+ return match?.[1] || path.basename(workingDir);
334
+ } catch {
335
+ return path.basename(workingDir);
336
+ }
337
+ }
338
+
339
+ private isWorktree(workingDir: string): boolean {
340
+ try {
341
+ const gitDir = path.join(workingDir, ".git");
342
+ if (fs.existsSync(gitDir) && fs.statSync(gitDir).isFile()) {
343
+ return true;
344
+ }
345
+ return false;
346
+ } catch {
347
+ return false;
348
+ }
349
+ }
350
+
351
+ private async getStatusWithBranchAsync(workingDir: string): Promise<{
352
+ branch: string | null;
353
+ status: "clean" | "dirty" | "conflicts";
354
+ workingTree?: {
355
+ staged: number;
356
+ unstaged: number;
357
+ untracked: number;
358
+ conflicts: number;
359
+ };
360
+ }> {
361
+ try {
362
+ debug(`[GIT-EXEC] Running git status in ${workingDir}`);
363
+ const result = await this.execGitAsync("git status --porcelain -b", {
364
+ cwd: workingDir,
365
+ encoding: "utf8",
366
+ timeout: 2000,
367
+ });
368
+ const output = result.stdout;
369
+ const lines = output.split("\n");
370
+
371
+ let branch: string | null = null;
372
+ let status: "clean" | "dirty" | "conflicts" = "clean";
373
+ let staged = 0;
374
+ let unstaged = 0;
375
+ let untracked = 0;
376
+ let conflicts = 0;
377
+
378
+ for (const line of lines) {
379
+ if (!line) continue;
380
+
381
+ if (line.startsWith("## ")) {
382
+ const branchLine = line.substring(3);
383
+ const branchMatch = branchLine.split("...")[0];
384
+ if (branchMatch && branchMatch !== "HEAD (no branch)") {
385
+ branch = branchMatch;
386
+ }
387
+ continue;
388
+ }
389
+
390
+ if (line.length >= 2) {
391
+ const indexStatus = line.charAt(0);
392
+ const worktreeStatus = line.charAt(1);
393
+
394
+ if (indexStatus === "?" && worktreeStatus === "?") {
395
+ untracked++;
396
+ if (status === "clean") status = "dirty";
397
+ continue;
398
+ }
399
+
400
+ const statusPair = indexStatus + worktreeStatus;
401
+ if (["DD", "AU", "UD", "UA", "DU", "AA", "UU"].includes(statusPair)) {
402
+ conflicts++;
403
+ status = "conflicts";
404
+ continue;
405
+ }
406
+
407
+ if (indexStatus !== " " && indexStatus !== "?") {
408
+ staged++;
409
+ if (status === "clean") status = "dirty";
410
+ }
411
+ if (worktreeStatus !== " " && worktreeStatus !== "?") {
412
+ unstaged++;
413
+ if (status === "clean") status = "dirty";
414
+ }
415
+ }
416
+ }
417
+
418
+ return {
419
+ branch: branch || (await this.getFallbackBranch(workingDir)),
420
+ status,
421
+ workingTree: { staged, unstaged, untracked, conflicts },
422
+ };
423
+ } catch (error) {
424
+ debug(`Git status with branch command failed in ${workingDir}:`, error);
425
+ return {
426
+ branch: await this.getFallbackBranch(workingDir),
427
+ status: "clean",
428
+ };
429
+ }
430
+ }
431
+
432
+ private async getFallbackBranch(workingDir: string): Promise<string | null> {
433
+ try {
434
+ const result = await this.execGitAsync("git branch --show-current", {
435
+ cwd: workingDir,
436
+ encoding: "utf8",
437
+ timeout: 2000,
438
+ });
439
+ const branch = result.stdout.trim();
440
+ if (branch) {
441
+ return branch;
442
+ }
443
+ } catch {
444
+ try {
445
+ const result = await this.execGitAsync(
446
+ "git symbolic-ref --short HEAD",
447
+ {
448
+ cwd: workingDir,
449
+ encoding: "utf8",
450
+ timeout: 2000,
451
+ },
452
+ );
453
+ const branch = result.stdout.trim();
454
+ if (branch) {
455
+ return branch;
456
+ }
457
+ } catch {
458
+ return null;
459
+ }
460
+ }
461
+ return null;
462
+ }
463
+
464
+ private async getAheadBehindAsync(workingDir: string): Promise<{
465
+ ahead: number;
466
+ behind: number;
467
+ }> {
468
+ try {
469
+ debug(`[GIT-EXEC] Running git ahead/behind in ${workingDir}`);
470
+ const [aheadResult, behindResult] = await Promise.all([
471
+ this.execGitAsync("git rev-list --count @{u}..HEAD", {
472
+ cwd: workingDir,
473
+ encoding: "utf8",
474
+ timeout: 2000,
475
+ }),
476
+ this.execGitAsync("git rev-list --count HEAD..@{u}", {
477
+ cwd: workingDir,
478
+ encoding: "utf8",
479
+ timeout: 2000,
480
+ }),
481
+ ]);
482
+
483
+ return {
484
+ ahead: parseInt(aheadResult.stdout.trim()) || 0,
485
+ behind: parseInt(behindResult.stdout.trim()) || 0,
486
+ };
487
+ } catch (error) {
488
+ debug(`Git ahead/behind command failed in ${workingDir}:`, error);
489
+ return { ahead: 0, behind: 0 };
490
+ }
491
+ }
492
+ }
@@ -0,0 +1,25 @@
1
+ export { GitService } from "./git";
2
+ export type { GitInfo } from "./git";
3
+ export { TmuxService } from "./tmux";
4
+ export { SessionProvider, UsageProvider } from "./session";
5
+ export type { SessionInfo, UsageInfo, TokenBreakdown } from "./session";
6
+ export { ContextProvider } from "./context";
7
+ export type { ContextInfo } from "./context";
8
+ export { MetricsProvider } from "./metrics";
9
+ export type { MetricsInfo } from "./metrics";
10
+ export { SegmentRenderer } from "./renderer";
11
+ export type {
12
+ PowerlineSymbols,
13
+ AnySegmentConfig,
14
+ DirectorySegmentConfig,
15
+ GitSegmentConfig,
16
+ UsageSegmentConfig,
17
+ ContextSegmentConfig,
18
+ MetricsSegmentConfig,
19
+ BlockSegmentConfig,
20
+ TodaySegmentConfig,
21
+ VersionSegmentConfig,
22
+ SessionIdSegmentConfig,
23
+ EnvSegmentConfig,
24
+ WeeklySegmentConfig,
25
+ } from "./renderer";
@@ -0,0 +1,175 @@
1
+ import type { ClaudeHookData } from "../utils/claude";
2
+
3
+ import { readFile } from "node:fs/promises";
4
+ import { debug } from "../utils/logger";
5
+ import { findTranscriptFile } from "../utils/claude";
6
+
7
+ export interface MetricsInfo {
8
+ responseTime: number | null;
9
+ lastResponseTime: number | null;
10
+ sessionDuration: number | null;
11
+ messageCount: number | null;
12
+ linesAdded: number | null;
13
+ linesRemoved: number | null;
14
+ }
15
+
16
+ interface TranscriptEntry {
17
+ timestamp: string;
18
+ type?: string;
19
+ message?: {
20
+ role?: string;
21
+ type?: string;
22
+ content?: Array<{
23
+ type?: string;
24
+ [key: string]: unknown;
25
+ }>;
26
+ usage?: {
27
+ input_tokens?: number;
28
+ output_tokens?: number;
29
+ cache_creation_input_tokens?: number;
30
+ cache_read_input_tokens?: number;
31
+ };
32
+ };
33
+ isSidechain?: boolean;
34
+ }
35
+
36
+ export class MetricsProvider {
37
+ private async loadTranscriptEntries(
38
+ sessionId: string,
39
+ ): Promise<TranscriptEntry[]> {
40
+ try {
41
+ const transcriptPath = await findTranscriptFile(sessionId);
42
+ if (!transcriptPath) {
43
+ debug(`No transcript found for session: ${sessionId}`);
44
+ return [];
45
+ }
46
+
47
+ debug(`Loading transcript from: ${transcriptPath}`);
48
+
49
+ const content = await readFile(transcriptPath, "utf-8");
50
+ const lines = content
51
+ .trim()
52
+ .split("\n")
53
+ .filter((line) => line.trim());
54
+
55
+ const entries: TranscriptEntry[] = [];
56
+
57
+ for (const line of lines) {
58
+ try {
59
+ const entry = JSON.parse(line) as TranscriptEntry;
60
+
61
+ if (entry.isSidechain === true) {
62
+ continue;
63
+ }
64
+
65
+ entries.push(entry);
66
+ } catch (parseError) {
67
+ debug(`Failed to parse JSONL line: ${parseError}`);
68
+ continue;
69
+ }
70
+ }
71
+
72
+ debug(`Loaded ${entries.length} transcript entries`);
73
+ return entries;
74
+ } catch (error) {
75
+ debug(`Error loading transcript for ${sessionId}:`, error);
76
+ return [];
77
+ }
78
+ }
79
+
80
+ private calculateMessageCount(entries: TranscriptEntry[]): number {
81
+ return entries.filter((entry) => {
82
+ const messageType =
83
+ entry.type || entry.message?.role || entry.message?.type;
84
+ const isToolResult =
85
+ entry.type === "user" &&
86
+ entry.message?.content?.[0]?.type === "tool_result";
87
+ return messageType === "user" && !isToolResult;
88
+ }).length;
89
+ }
90
+
91
+ private calculateLastResponseTime(entries: TranscriptEntry[]): number | null {
92
+ if (entries.length === 0) return null;
93
+
94
+ const recentEntries = entries.slice(-20);
95
+
96
+ let lastUserTime: Date | null = null;
97
+ let bestResponseTime: number | null = null;
98
+
99
+ for (const entry of recentEntries) {
100
+ if (!entry.timestamp) continue;
101
+
102
+ try {
103
+ const timestamp = new Date(entry.timestamp);
104
+ const messageType =
105
+ entry.type || entry.message?.role || entry.message?.type;
106
+
107
+ const isToolResult =
108
+ entry.type === "user" &&
109
+ entry.message?.content?.[0]?.type === "tool_result";
110
+ const isRealUserMessage = messageType === "user" && !isToolResult;
111
+
112
+ if (isRealUserMessage) {
113
+ lastUserTime = timestamp;
114
+ } else if (messageType === "assistant" && lastUserTime) {
115
+ const responseTime =
116
+ (timestamp.getTime() - lastUserTime.getTime()) / 1000;
117
+ if (responseTime > 0.1 && responseTime < 300) {
118
+ bestResponseTime = responseTime;
119
+ }
120
+ }
121
+ } catch {
122
+ continue;
123
+ }
124
+ }
125
+
126
+ return bestResponseTime;
127
+ }
128
+
129
+ async getMetricsInfo(
130
+ sessionId: string,
131
+ hookData: ClaudeHookData,
132
+ ): Promise<MetricsInfo> {
133
+ try {
134
+ debug(`Getting metrics from hook data for session: ${sessionId}`);
135
+
136
+ if (!hookData.cost) {
137
+ debug(`No cost data available in hook data`);
138
+ return {
139
+ responseTime: null,
140
+ lastResponseTime: null,
141
+ sessionDuration: null,
142
+ messageCount: null,
143
+ linesAdded: null,
144
+ linesRemoved: null,
145
+ };
146
+ }
147
+
148
+ const entries = await this.loadTranscriptEntries(sessionId);
149
+ const messageCount = this.calculateMessageCount(entries);
150
+ const lastResponseTime = this.calculateLastResponseTime(entries);
151
+
152
+ return {
153
+ responseTime: hookData.cost.total_api_duration_ms / 1000,
154
+ lastResponseTime,
155
+ sessionDuration: hookData.cost.total_duration_ms / 1000,
156
+ messageCount,
157
+ linesAdded: hookData.cost.total_lines_added,
158
+ linesRemoved: hookData.cost.total_lines_removed,
159
+ };
160
+ } catch (error) {
161
+ debug(
162
+ `Error getting metrics from hook data for session ${sessionId}:`,
163
+ error,
164
+ );
165
+ return {
166
+ responseTime: null,
167
+ lastResponseTime: null,
168
+ sessionDuration: null,
169
+ messageCount: null,
170
+ linesAdded: null,
171
+ linesRemoved: null,
172
+ };
173
+ }
174
+ }
175
+ }