claude-code-wakatime 3.0.2 → 3.0.4
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 +400 -495
- package/package.json +1 -1
- package/src/dependencies.ts +8 -8
- package/src/index.ts +27 -205
- package/src/logger.ts +8 -0
- package/src/options.ts +23 -20
- package/src/types.ts +50 -0
- package/src/utils.ts +118 -71
package/package.json
CHANGED
package/src/dependencies.ts
CHANGED
|
@@ -8,8 +8,8 @@ import * as semver from 'semver';
|
|
|
8
8
|
import * as which from 'which';
|
|
9
9
|
|
|
10
10
|
import { Options, Setting } from './options';
|
|
11
|
-
import { Utils } from './utils';
|
|
12
11
|
import { Logger } from './logger';
|
|
12
|
+
import { buildOptions, isWindows } from './utils';
|
|
13
13
|
|
|
14
14
|
enum osName {
|
|
15
15
|
darwin = 'darwin',
|
|
@@ -49,7 +49,7 @@ export class Dependencies {
|
|
|
49
49
|
|
|
50
50
|
const osname = this.osName();
|
|
51
51
|
const arch = this.architecture();
|
|
52
|
-
const ext =
|
|
52
|
+
const ext = isWindows() ? '.exe' : '';
|
|
53
53
|
const binary = `wakatime-cli-${osname}-${arch}${ext}`;
|
|
54
54
|
this.cliLocation = path.join(this.resourcesLocation, binary);
|
|
55
55
|
|
|
@@ -59,7 +59,7 @@ export class Dependencies {
|
|
|
59
59
|
public getCliLocationGlobal(): string | undefined {
|
|
60
60
|
if (this.cliLocationGlobal) return this.cliLocationGlobal;
|
|
61
61
|
|
|
62
|
-
const binaryName = `wakatime-cli${
|
|
62
|
+
const binaryName = `wakatime-cli${isWindows() ? '.exe' : ''}`;
|
|
63
63
|
const path = which.sync(binaryName, { nothrow: true });
|
|
64
64
|
if (path) {
|
|
65
65
|
this.cliLocationGlobal = path;
|
|
@@ -96,10 +96,10 @@ export class Dependencies {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
let args = ['--version'];
|
|
99
|
-
const options =
|
|
99
|
+
const options = buildOptions();
|
|
100
100
|
try {
|
|
101
101
|
child_process.execFile(this.getCliLocation(), args, options, (error, _stdout, stderr) => {
|
|
102
|
-
if (!
|
|
102
|
+
if (!error) {
|
|
103
103
|
let currentVersion = _stdout.toString().trim() + stderr.toString().trim();
|
|
104
104
|
this.logger.debug(`Current wakatime-cli version is ${currentVersion}`);
|
|
105
105
|
|
|
@@ -212,7 +212,7 @@ export class Dependencies {
|
|
|
212
212
|
this.unzip(zipFile, this.resourcesLocation, (unzipped) => {
|
|
213
213
|
if (!unzipped) {
|
|
214
214
|
this.restoreCli();
|
|
215
|
-
} else if (!
|
|
215
|
+
} else if (!isWindows()) {
|
|
216
216
|
this.removeCli();
|
|
217
217
|
const cli = this.getCliLocation();
|
|
218
218
|
try {
|
|
@@ -221,7 +221,7 @@ export class Dependencies {
|
|
|
221
221
|
} catch (e) {
|
|
222
222
|
this.logger.warnException(e);
|
|
223
223
|
}
|
|
224
|
-
const ext =
|
|
224
|
+
const ext = isWindows() ? '.exe' : '';
|
|
225
225
|
const link = path.join(this.resourcesLocation, `wakatime-cli${ext}`);
|
|
226
226
|
if (!this.isSymlink(link)) {
|
|
227
227
|
try {
|
|
@@ -288,7 +288,7 @@ export class Dependencies {
|
|
|
288
288
|
});
|
|
289
289
|
} catch (e) {
|
|
290
290
|
this.logger.warnException(e);
|
|
291
|
-
|
|
291
|
+
error();
|
|
292
292
|
}
|
|
293
293
|
}
|
|
294
294
|
|
package/src/index.ts
CHANGED
|
@@ -1,199 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import os from 'os';
|
|
6
3
|
import { execFile } from 'child_process';
|
|
7
4
|
import { Options } from './options';
|
|
8
5
|
import { VERSION } from './version';
|
|
9
6
|
import { Dependencies } from './dependencies';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
7
|
+
import { logger, Logger, LogLevel } from './logger';
|
|
8
|
+
import { Input } from './types';
|
|
9
|
+
import { buildOptions, formatArguments, getEntityFiles, parseInput, shouldSendHeartbeat, updateState } from './utils';
|
|
12
10
|
|
|
13
|
-
const STATE_FILE = path.join(os.homedir(), '.wakatime', 'claude-code.json');
|
|
14
|
-
const logger = new Logger();
|
|
15
11
|
const options = new Options();
|
|
16
12
|
const deps = new Dependencies(options, logger);
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
lastHeartbeatAt?: number;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
type Input = {
|
|
23
|
-
session_id: string;
|
|
24
|
-
transcript_path: string;
|
|
25
|
-
cwd: string;
|
|
26
|
-
hook_event_name: string;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
function shouldSendHeartbeat(inp?: Input): boolean {
|
|
30
|
-
if (inp?.hook_event_name === 'Stop') {
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
const last = (JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8')) as State).lastHeartbeatAt ?? Utils.timestamp();
|
|
36
|
-
return Utils.timestamp() - last >= 60;
|
|
37
|
-
} catch {
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function parseInput() {
|
|
43
|
-
try {
|
|
44
|
-
const stdinData = fs.readFileSync(0, 'utf-8');
|
|
45
|
-
if (stdinData.trim()) {
|
|
46
|
-
const input: Input = JSON.parse(stdinData);
|
|
47
|
-
return input;
|
|
48
|
-
}
|
|
49
|
-
} catch (err) {
|
|
50
|
-
console.error(err);
|
|
51
|
-
}
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function getLastHeartbeat() {
|
|
56
|
-
try {
|
|
57
|
-
const stateData = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8')) as State;
|
|
58
|
-
return stateData.lastHeartbeatAt ?? 0;
|
|
59
|
-
} catch {
|
|
60
|
-
return 0;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function getModifiedFile(transcriptPath: string): string | undefined {
|
|
65
|
-
try {
|
|
66
|
-
if (!transcriptPath || !fs.existsSync(transcriptPath)) {
|
|
67
|
-
return undefined;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const content = fs.readFileSync(transcriptPath, 'utf-8');
|
|
71
|
-
const lines = content.split('\n');
|
|
72
|
-
const fileLineChanges = new Map<string, number>();
|
|
73
|
-
|
|
74
|
-
const lastHeartbeatAt = getLastHeartbeat();
|
|
75
|
-
for (const line of lines) {
|
|
76
|
-
if (line.trim()) {
|
|
77
|
-
try {
|
|
78
|
-
const logEntry = JSON.parse(line);
|
|
79
|
-
if (!logEntry.timestamp) continue;
|
|
80
|
-
|
|
81
|
-
const entryTimestamp = new Date(logEntry.timestamp).getTime() / 1000;
|
|
82
|
-
if (entryTimestamp >= lastHeartbeatAt) {
|
|
83
|
-
let filePath: string | undefined;
|
|
84
|
-
|
|
85
|
-
// Check for file paths in tool use results
|
|
86
|
-
if (logEntry.toolUse?.parameters?.file_path) {
|
|
87
|
-
filePath = logEntry.toolUse.parameters.file_path;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Check for file paths in tool use results for multi-edit
|
|
91
|
-
if (logEntry.toolUse?.parameters?.edits) {
|
|
92
|
-
filePath = logEntry.toolUse.parameters.file_path;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Check for file paths and line changes in structured patch
|
|
96
|
-
if (logEntry.toolUseResult?.structuredPatch) {
|
|
97
|
-
const patches = logEntry.toolUseResult.structuredPatch;
|
|
98
|
-
for (const patch of patches) {
|
|
99
|
-
if (patch.file) {
|
|
100
|
-
filePath = patch.file as string;
|
|
101
|
-
if (patch.newLines !== undefined && patch.oldLines !== undefined) {
|
|
102
|
-
const lineChanges = Math.abs(patch.newLines - patch.oldLines);
|
|
103
|
-
fileLineChanges.set(filePath, (fileLineChanges.get(filePath) || 0) + lineChanges);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (filePath && !fileLineChanges.has(filePath)) {
|
|
110
|
-
fileLineChanges.set(filePath, 0);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
} catch {
|
|
114
|
-
// ignore
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (fileLineChanges.size === 0) {
|
|
120
|
-
return undefined;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Find file with most line changes
|
|
124
|
-
let maxChanges = 0;
|
|
125
|
-
let mostChangedFile: string | undefined;
|
|
126
|
-
for (const [file, changes] of fileLineChanges.entries()) {
|
|
127
|
-
if (changes > maxChanges) {
|
|
128
|
-
maxChanges = changes;
|
|
129
|
-
mostChangedFile = file;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return mostChangedFile;
|
|
134
|
-
} catch {
|
|
135
|
-
return undefined;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function calculateLineChanges(transcriptPath: string): number {
|
|
140
|
-
try {
|
|
141
|
-
if (!transcriptPath || !fs.existsSync(transcriptPath)) {
|
|
142
|
-
return 0;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const content = fs.readFileSync(transcriptPath, 'utf-8');
|
|
146
|
-
const lines = content.split('\n');
|
|
147
|
-
let totalLineChanges = 0;
|
|
148
|
-
|
|
149
|
-
const lastHeartbeatAt = getLastHeartbeat();
|
|
150
|
-
for (const line of lines) {
|
|
151
|
-
if (line.trim()) {
|
|
152
|
-
try {
|
|
153
|
-
const logEntry = JSON.parse(line);
|
|
154
|
-
|
|
155
|
-
// Only count changes since last heartbeat
|
|
156
|
-
if (logEntry.timestamp && logEntry.toolUseResult?.structuredPatch) {
|
|
157
|
-
const entryTimestamp = new Date(logEntry.timestamp).getTime() / 1000;
|
|
158
|
-
if (entryTimestamp >= lastHeartbeatAt) {
|
|
159
|
-
const patches = logEntry.toolUseResult.structuredPatch;
|
|
160
|
-
for (const patch of patches) {
|
|
161
|
-
if (patch.newLines !== undefined && patch.oldLines !== undefined) {
|
|
162
|
-
totalLineChanges += patch.newLines - patch.oldLines;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
} catch {
|
|
168
|
-
// ignore
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return totalLineChanges;
|
|
174
|
-
} catch {
|
|
175
|
-
return 0;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function updateState() {
|
|
180
|
-
fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
|
|
181
|
-
fs.writeFileSync(STATE_FILE, JSON.stringify({ lastHeartbeatAt: Utils.timestamp() } as State, null, 2));
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function getEntityFile(inp: Input | undefined): string | undefined {
|
|
185
|
-
if (!inp?.transcript_path) return;
|
|
186
|
-
return getModifiedFile(inp.transcript_path);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function sendHeartbeat(inp: Input | undefined) {
|
|
14
|
+
function sendHeartbeat(inp: Input | undefined): boolean {
|
|
190
15
|
const projectFolder = inp?.cwd;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (!entity) return;
|
|
16
|
+
const { entities, claudeVersion } = getEntityFiles(inp);
|
|
17
|
+
if (entities.size === 0) return false;
|
|
194
18
|
|
|
195
|
-
|
|
19
|
+
const wakatime_cli = deps.getCliLocation();
|
|
196
20
|
|
|
21
|
+
for (const [entity, lineChanges] of entities.entries()) {
|
|
22
|
+
logger.debug(`Entity: ${entity}`);
|
|
197
23
|
const args: string[] = [
|
|
198
24
|
'--entity',
|
|
199
25
|
entity,
|
|
@@ -202,32 +28,29 @@ function sendHeartbeat(inp: Input | undefined) {
|
|
|
202
28
|
'--category',
|
|
203
29
|
'ai coding',
|
|
204
30
|
'--plugin',
|
|
205
|
-
`claude-code-wakatime/${VERSION}`,
|
|
31
|
+
`claude/${claudeVersion} claude-code-wakatime/${VERSION}`,
|
|
206
32
|
];
|
|
207
33
|
if (projectFolder) {
|
|
208
34
|
args.push('--project-folder');
|
|
209
35
|
args.push(projectFolder);
|
|
210
36
|
}
|
|
211
37
|
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
args.push('--ai-line-changes');
|
|
216
|
-
args.push(lineChanges.toString());
|
|
217
|
-
}
|
|
38
|
+
if (lineChanges) {
|
|
39
|
+
args.push('--ai-line-changes');
|
|
40
|
+
args.push(lineChanges.toString());
|
|
218
41
|
}
|
|
219
42
|
|
|
220
|
-
logger.debug(`Sending heartbeat: ${wakatime_cli
|
|
43
|
+
logger.debug(`Sending heartbeat: ${formatArguments(wakatime_cli, args)}`);
|
|
221
44
|
|
|
222
|
-
const execOptions =
|
|
45
|
+
const execOptions = buildOptions();
|
|
223
46
|
execFile(wakatime_cli, args, execOptions, (error, stdout, stderr) => {
|
|
224
47
|
const output = stdout.toString().trim() + stderr.toString().trim();
|
|
225
48
|
if (output) logger.error(output);
|
|
226
49
|
if (error) logger.error(error.toString());
|
|
227
50
|
});
|
|
228
|
-
} catch (err: any) {
|
|
229
|
-
logger.errorException(err);
|
|
230
51
|
}
|
|
52
|
+
|
|
53
|
+
return true;
|
|
231
54
|
}
|
|
232
55
|
|
|
233
56
|
function main() {
|
|
@@ -236,19 +59,18 @@ function main() {
|
|
|
236
59
|
const debug = options.getSetting('settings', 'debug');
|
|
237
60
|
logger.setLevel(debug === 'true' ? LogLevel.DEBUG : LogLevel.INFO);
|
|
238
61
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
logger.debug(JSON.stringify(inp, null, 2));
|
|
242
|
-
} catch (err) {
|
|
243
|
-
// ignore
|
|
244
|
-
}
|
|
245
|
-
}
|
|
62
|
+
try {
|
|
63
|
+
if (inp) logger.debug(JSON.stringify(inp, null, 2));
|
|
246
64
|
|
|
247
|
-
|
|
65
|
+
deps.checkAndInstallCli();
|
|
248
66
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
67
|
+
if (shouldSendHeartbeat(inp)) {
|
|
68
|
+
if (sendHeartbeat(inp)) {
|
|
69
|
+
updateState();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
logger.errorException(err);
|
|
252
74
|
}
|
|
253
75
|
}
|
|
254
76
|
|
package/src/logger.ts
CHANGED
package/src/options.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import * as child_process from 'child_process';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
-
|
|
5
|
-
import { Utils } from './utils';
|
|
4
|
+
import { getHomeDirectory } from './utils';
|
|
6
5
|
|
|
7
6
|
export interface Setting {
|
|
8
7
|
key: string;
|
|
@@ -17,7 +16,7 @@ export class Options {
|
|
|
17
16
|
public resourcesLocation: string;
|
|
18
17
|
|
|
19
18
|
constructor() {
|
|
20
|
-
const home =
|
|
19
|
+
const home = getHomeDirectory();
|
|
21
20
|
const wakaFolder = path.join(home, '.wakatime');
|
|
22
21
|
try {
|
|
23
22
|
if (!fs.existsSync(wakaFolder)) {
|
|
@@ -35,26 +34,30 @@ export class Options {
|
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
public getSetting(section: string, key: string, internal?: boolean): string | undefined {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
37
|
+
try {
|
|
38
|
+
const content = fs.readFileSync(this.getConfigFile(internal ?? false), 'utf-8');
|
|
39
|
+
if (content.trim()) {
|
|
40
|
+
let currentSection = '';
|
|
41
|
+
let lines = content.split('\n');
|
|
42
|
+
for (var i = 0; i < lines.length; i++) {
|
|
43
|
+
let line = lines[i];
|
|
44
|
+
if (this.startsWith(line.trim(), '[') && this.endsWith(line.trim(), ']')) {
|
|
45
|
+
currentSection = line
|
|
46
|
+
.trim()
|
|
47
|
+
.substring(1, line.trim().length - 1)
|
|
48
|
+
.toLowerCase();
|
|
49
|
+
} else if (currentSection === section) {
|
|
50
|
+
let parts = line.split('=');
|
|
51
|
+
let currentKey = parts[0].trim();
|
|
52
|
+
if (currentKey === key && parts.length > 1) {
|
|
53
|
+
return this.removeNulls(parts[1].trim());
|
|
54
|
+
}
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
|
-
}
|
|
57
57
|
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
} catch (_) {
|
|
58
61
|
return undefined;
|
|
59
62
|
}
|
|
60
63
|
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type State = {
|
|
2
|
+
lastHeartbeatAt?: number;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export type Input = {
|
|
6
|
+
session_id: string;
|
|
7
|
+
transcript_path: string;
|
|
8
|
+
cwd: string;
|
|
9
|
+
hook_event_name: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type TranscriptLog = {
|
|
13
|
+
parentUuid?: string;
|
|
14
|
+
isSidechain?: boolean;
|
|
15
|
+
userType?: string;
|
|
16
|
+
cwd?: string;
|
|
17
|
+
sessionId?: string;
|
|
18
|
+
version?: string;
|
|
19
|
+
gitBranch?: string;
|
|
20
|
+
type?: string;
|
|
21
|
+
message?: {
|
|
22
|
+
role?: string;
|
|
23
|
+
content?: [
|
|
24
|
+
{
|
|
25
|
+
tool_use_id: string;
|
|
26
|
+
type: string;
|
|
27
|
+
content: string;
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
};
|
|
31
|
+
uuid?: string;
|
|
32
|
+
timestamp?: string;
|
|
33
|
+
toolUseResult: {
|
|
34
|
+
filePath?: string;
|
|
35
|
+
oldString?: string;
|
|
36
|
+
newString?: string;
|
|
37
|
+
originalFile?: string;
|
|
38
|
+
structuredPatch?: [
|
|
39
|
+
{
|
|
40
|
+
oldStart: number;
|
|
41
|
+
oldLines: number;
|
|
42
|
+
newStart: number;
|
|
43
|
+
newLines: number;
|
|
44
|
+
lines: string[];
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
userModified?: string;
|
|
48
|
+
replaceAll?: string;
|
|
49
|
+
};
|
|
50
|
+
};
|