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/.claude-plugin/marketplace.json +4 -1
- package/dist/index.js +1342 -1443
- package/dist/install-hooks.js +71 -0
- package/hooks/hooks.json +1 -12
- package/package.json +3 -2
- package/src/dependencies.ts +8 -8
- package/src/index.ts +30 -209
- package/src/install-hooks.ts +1 -10
- package/src/logger.ts +8 -1
- package/src/options.ts +2 -3
- package/src/types.ts +50 -0
- package/src/utils.ts +118 -71
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
}
|