claude-code-wakatime 3.0.1 → 3.0.3

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/src/utils.ts CHANGED
@@ -2,96 +2,143 @@ import * as fs from 'fs';
2
2
  import * as os from 'os';
3
3
  import * as child_process from 'child_process';
4
4
  import { StdioOptions } from 'child_process';
5
+ import { Input, State, TranscriptLog } from './types';
6
+ import path from 'path';
7
+ import { logger } from './logger';
5
8
 
6
- export class Utils {
7
- public static quote(str: string): string {
8
- if (str.includes(' ')) return `"${str.replace('"', '\\"')}"`;
9
- return str;
10
- }
9
+ const STATE_FILE = path.join(os.homedir(), '.wakatime', 'claude-code.json');
11
10
 
12
- public static apiKeyInvalid(key?: string): string {
13
- const err = 'Invalid api key... check https://wakatime.com/api-key for your key';
14
- if (!key) return err;
15
- const re = new RegExp('^(waka_)?[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$', 'i');
16
- if (!re.test(key)) return err;
17
- return '';
11
+ export function parseInput() {
12
+ try {
13
+ const stdinData = fs.readFileSync(0, 'utf-8');
14
+ if (stdinData.trim()) {
15
+ const input: Input = JSON.parse(stdinData);
16
+ return input;
17
+ }
18
+ } catch (err) {
19
+ console.error(err);
18
20
  }
21
+ return undefined;
22
+ }
19
23
 
20
- public static formatDate(date: Date): String {
21
- let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
22
- let ampm = 'AM';
23
- let hour = date.getHours();
24
- if (hour > 11) {
25
- ampm = 'PM';
26
- hour = hour - 12;
27
- }
28
- if (hour == 0) {
29
- hour = 12;
30
- }
31
- let minute = date.getMinutes();
32
- return `${months[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()} ${hour}:${minute < 10 ? `0${minute}` : minute} ${ampm}`;
24
+ export function shouldSendHeartbeat(inp?: Input): boolean {
25
+ if (inp?.hook_event_name === 'Stop') {
26
+ return true;
33
27
  }
34
28
 
35
- public static obfuscateKey(key: string): string {
36
- let newKey = '';
37
- if (key) {
38
- newKey = key;
39
- if (key.length > 4) newKey = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX' + key.substring(key.length - 4);
40
- }
41
- return newKey;
29
+ try {
30
+ const last = (JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8')) as State).lastHeartbeatAt ?? timestamp();
31
+ return timestamp() - last >= 60;
32
+ } catch {
33
+ return true;
42
34
  }
35
+ }
36
+
37
+ export function updateState() {
38
+ fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
39
+ fs.writeFileSync(STATE_FILE, JSON.stringify({ lastHeartbeatAt: timestamp() } as State, null, 2));
40
+ }
43
41
 
44
- public static wrapArg(arg: string): string {
45
- if (arg.indexOf(' ') > -1) return '"' + arg.replace(/"/g, '\\"') + '"';
46
- return arg;
42
+ export function getEntityFiles(inp: Input | undefined): { entities: Map<string, number>; claudeVersion: string } {
43
+ const entities = new Map<string, number>();
44
+ let claudeVersion = '';
45
+
46
+ const transcriptPath = inp?.transcript_path;
47
+ if (!transcriptPath || !fs.existsSync(transcriptPath)) {
48
+ return { entities, claudeVersion };
47
49
  }
48
50
 
49
- public static formatArguments(binary: string, args: string[]): string {
50
- let clone = args.slice(0);
51
- clone.unshift(this.wrapArg(binary));
52
- let newCmds: string[] = [];
53
- let lastCmd = '';
54
- for (let i = 0; i < clone.length; i++) {
55
- if (lastCmd == '--key') newCmds.push(this.wrapArg(this.obfuscateKey(clone[i])));
56
- else newCmds.push(this.wrapArg(clone[i]));
57
- lastCmd = clone[i];
51
+ const lastHeartbeatAt = getLastHeartbeat();
52
+
53
+ const content = fs.readFileSync(transcriptPath, 'utf-8');
54
+ for (const logLine of content.split('\n')) {
55
+ if (!logLine.trim()) continue;
56
+
57
+ try {
58
+ const log = JSON.parse(logLine) as TranscriptLog;
59
+ if (!log.timestamp) continue;
60
+
61
+ if (log.version) claudeVersion = log.version;
62
+
63
+ const timestamp = new Date(log.timestamp).getTime() / 1000;
64
+ if (timestamp < lastHeartbeatAt) continue;
65
+
66
+ const filePath = log.toolUseResult?.filePath;
67
+ if (!filePath) continue;
68
+
69
+ const patches = log.toolUseResult?.structuredPatch;
70
+ if (!patches) continue;
71
+
72
+ const lineChanges = patches.map((patch) => patch.newLines - patch.oldLines).reduce((p, c) => p + c, 0);
73
+
74
+ entities.set(filePath, (entities.get(filePath) ?? 0) + lineChanges);
75
+ } catch (err) {
76
+ logger.warnException(err);
58
77
  }
59
- return newCmds.join(' ');
60
78
  }
61
79
 
62
- public static apiUrlToDashboardUrl(url: string): string {
63
- url = url
64
- .replace('://api.', '://')
65
- .replace('/api/v1', '')
66
- .replace(/^api\./, '')
67
- .replace('/api', '');
68
- return url;
69
- }
80
+ return { entities, claudeVersion };
81
+ }
70
82
 
71
- public static isWindows(): boolean {
72
- return os.platform() === 'win32';
83
+ export function formatArguments(binary: string, args: string[]): string {
84
+ let clone = args.slice(0);
85
+ clone.unshift(wrapArg(binary));
86
+ let newCmds: string[] = [];
87
+ let lastCmd = '';
88
+ for (let i = 0; i < clone.length; i++) {
89
+ if (lastCmd == '--key') newCmds.push(wrapArg(obfuscateKey(clone[i])));
90
+ else newCmds.push(wrapArg(clone[i]));
91
+ lastCmd = clone[i];
73
92
  }
93
+ return newCmds.join(' ');
94
+ }
74
95
 
75
- public static getHomeDirectory(): string {
76
- let home = process.env.WAKATIME_HOME;
77
- if (home && home.trim() && fs.existsSync(home.trim())) return home.trim();
78
- return process.env[this.isWindows() ? 'USERPROFILE' : 'HOME'] || process.cwd();
96
+ export function isWindows(): boolean {
97
+ return os.platform() === 'win32';
98
+ }
99
+
100
+ export function getHomeDirectory(): string {
101
+ let home = process.env.WAKATIME_HOME;
102
+ if (home && home.trim() && fs.existsSync(home.trim())) return home.trim();
103
+ return process.env[isWindows() ? 'USERPROFILE' : 'HOME'] || process.cwd();
104
+ }
105
+
106
+ export function buildOptions(stdin?: boolean): Object {
107
+ const options: child_process.ExecFileOptions = {
108
+ windowsHide: true,
109
+ };
110
+ if (stdin) {
111
+ (options as any).stdio = ['pipe', 'pipe', 'pipe'] as StdioOptions;
112
+ }
113
+ if (!isWindows() && !process.env.WAKATIME_HOME && !process.env.HOME) {
114
+ options['env'] = { ...process.env, WAKATIME_HOME: getHomeDirectory() };
79
115
  }
116
+ return options;
117
+ }
80
118
 
81
- public static buildOptions(stdin?: boolean): Object {
82
- const options: child_process.ExecFileOptions = {
83
- windowsHide: true,
84
- };
85
- if (stdin) {
86
- (options as any).stdio = ['pipe', 'pipe', 'pipe'] as StdioOptions;
87
- }
88
- if (!this.isWindows() && !process.env.WAKATIME_HOME && !process.env.HOME) {
89
- options['env'] = { ...process.env, WAKATIME_HOME: this.getHomeDirectory() };
90
- }
91
- return options;
119
+ function getLastHeartbeat() {
120
+ try {
121
+ const stateData = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8')) as State;
122
+ return stateData.lastHeartbeatAt ?? 0;
123
+ } catch {
124
+ return 0;
92
125
  }
126
+ }
127
+
128
+ function timestamp() {
129
+ return Date.now() / 1000;
130
+ }
131
+
132
+ function wrapArg(arg: string): string {
133
+ if (arg.indexOf(' ') > -1) return '"' + arg.replace(/"/g, '\\"') + '"';
134
+ return arg;
135
+ }
93
136
 
94
- public static timestamp() {
95
- return Date.now() / 1000;
137
+ function obfuscateKey(key: string): string {
138
+ let newKey = '';
139
+ if (key) {
140
+ newKey = key;
141
+ if (key.length > 4) newKey = 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXX' + key.substring(key.length - 4);
96
142
  }
143
+ return newKey;
97
144
  }