claude-code-wakatime 1.0.1 → 2.1.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.
- package/README.md +6 -1
- package/dist/dependencies.js +416 -0
- package/dist/index.js +78 -21
- package/dist/install-hooks.js +1 -1
- package/dist/logger.js +75 -0
- package/dist/utils.js +3 -0
- package/dist/version.js +5 -0
- package/package.json +12 -2
- package/src/dependencies.ts +395 -0
- package/src/index.ts +82 -18
- package/src/install-hooks.ts +9 -10
- package/src/logger.ts +77 -0
- package/src/options.ts +1 -1
- package/src/utils.ts +4 -0
package/README.md
CHANGED
|
@@ -6,12 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
1. `npm install -g claude-code-wakatime`
|
|
8
8
|
|
|
9
|
-
2.
|
|
9
|
+
2. Add your [api key](https://wakatime.com/settings/api-key) to `~/.wakatime.cfg`:
|
|
10
|
+
|
|
11
|
+
[settings]
|
|
12
|
+
api_key = waka_123
|
|
10
13
|
|
|
11
14
|
4. Use Claude Code and your AI coding activity will be displayed on your [WakaTime dashboard](https://wakatime.com)
|
|
12
15
|
|
|
13
16
|
## Usage
|
|
14
17
|
|
|
18
|
+
New: See the lines of code generated by AI on your dashboard!
|
|
19
|
+
|
|
15
20
|
Visit [https://wakatime.com](https://wakatime.com) to see your coding activity.
|
|
16
21
|
|
|
17
22
|

|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.Dependencies = void 0;
|
|
40
|
+
const adm_zip_1 = __importDefault(require("adm-zip"));
|
|
41
|
+
const child_process = __importStar(require("child_process"));
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const os = __importStar(require("os"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const request = __importStar(require("request"));
|
|
46
|
+
const semver = __importStar(require("semver"));
|
|
47
|
+
const which = __importStar(require("which"));
|
|
48
|
+
const utils_1 = require("./utils");
|
|
49
|
+
var osName;
|
|
50
|
+
(function (osName) {
|
|
51
|
+
osName["darwin"] = "darwin";
|
|
52
|
+
osName["windows"] = "windows";
|
|
53
|
+
osName["linux"] = "linux";
|
|
54
|
+
})(osName || (osName = {}));
|
|
55
|
+
class Dependencies {
|
|
56
|
+
constructor(options, logger) {
|
|
57
|
+
this.cliLocation = undefined;
|
|
58
|
+
this.cliLocationGlobal = undefined;
|
|
59
|
+
this.cliInstalled = false;
|
|
60
|
+
this.githubDownloadUrl = 'https://github.com/wakatime/wakatime-cli/releases/latest/download';
|
|
61
|
+
this.githubReleasesUrl = 'https://api.github.com/repos/wakatime/wakatime-cli/releases/latest';
|
|
62
|
+
this.legacyOperatingSystems = {
|
|
63
|
+
[osName.darwin]: [{ kernelLessThan: '17.0.0', tag: 'v1.39.1-alpha.1' }],
|
|
64
|
+
};
|
|
65
|
+
this.options = options;
|
|
66
|
+
this.logger = logger;
|
|
67
|
+
this.resourcesLocation = options.resourcesLocation;
|
|
68
|
+
}
|
|
69
|
+
getCliLocation() {
|
|
70
|
+
if (this.cliLocation)
|
|
71
|
+
return this.cliLocation;
|
|
72
|
+
this.cliLocation = this.getCliLocationGlobal();
|
|
73
|
+
if (this.cliLocation)
|
|
74
|
+
return this.cliLocation;
|
|
75
|
+
const osname = this.osName();
|
|
76
|
+
const arch = this.architecture();
|
|
77
|
+
const ext = utils_1.Utils.isWindows() ? '.exe' : '';
|
|
78
|
+
const binary = `wakatime-cli-${osname}-${arch}${ext}`;
|
|
79
|
+
this.cliLocation = path.join(this.resourcesLocation, binary);
|
|
80
|
+
return this.cliLocation;
|
|
81
|
+
}
|
|
82
|
+
getCliLocationGlobal() {
|
|
83
|
+
if (this.cliLocationGlobal)
|
|
84
|
+
return this.cliLocationGlobal;
|
|
85
|
+
const binaryName = `wakatime-cli${utils_1.Utils.isWindows() ? '.exe' : ''}`;
|
|
86
|
+
const path = which.sync(binaryName, { nothrow: true });
|
|
87
|
+
if (path) {
|
|
88
|
+
this.cliLocationGlobal = path;
|
|
89
|
+
this.logger.debug(`Using global wakatime-cli location: ${path}`);
|
|
90
|
+
}
|
|
91
|
+
return this.cliLocationGlobal;
|
|
92
|
+
}
|
|
93
|
+
isCliInstalled() {
|
|
94
|
+
if (this.cliInstalled)
|
|
95
|
+
return true;
|
|
96
|
+
this.cliInstalled = fs.existsSync(this.getCliLocation());
|
|
97
|
+
return this.cliInstalled;
|
|
98
|
+
}
|
|
99
|
+
checkAndInstallCli(callback) {
|
|
100
|
+
if (!this.isCliInstalled()) {
|
|
101
|
+
this.installCli(callback ?? (() => { }));
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
this.isCliLatest((isLatest) => {
|
|
105
|
+
if (!isLatest) {
|
|
106
|
+
this.installCli(callback ?? (() => { }));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
callback?.();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
isCliLatest(callback) {
|
|
115
|
+
if (this.getCliLocationGlobal()) {
|
|
116
|
+
callback(true);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
let args = ['--version'];
|
|
120
|
+
const options = utils_1.Utils.buildOptions();
|
|
121
|
+
try {
|
|
122
|
+
child_process.execFile(this.getCliLocation(), args, options, (error, _stdout, stderr) => {
|
|
123
|
+
if (!(error != null)) {
|
|
124
|
+
let currentVersion = _stdout.toString().trim() + stderr.toString().trim();
|
|
125
|
+
this.logger.debug(`Current wakatime-cli version is ${currentVersion}`);
|
|
126
|
+
if (currentVersion === '<local-build>') {
|
|
127
|
+
callback(true);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const tag = this.legacyReleaseTag();
|
|
131
|
+
if (tag && currentVersion !== tag) {
|
|
132
|
+
callback(false);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const accessed = this.options.getSetting('internal', 'cli_version_last_accessed', true);
|
|
136
|
+
const now = Math.round(Date.now() / 1000);
|
|
137
|
+
const lastAccessed = parseInt(accessed ?? '0');
|
|
138
|
+
const fourHours = 4 * 3600;
|
|
139
|
+
if (lastAccessed && lastAccessed + fourHours > now) {
|
|
140
|
+
this.logger.debug(`Skip checking for wakatime-cli updates because recently checked ${now - lastAccessed} seconds ago.`);
|
|
141
|
+
callback(true);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
this.logger.debug('Checking for updates to wakatime-cli...');
|
|
145
|
+
this.getLatestCliVersion((latestVersion) => {
|
|
146
|
+
if (currentVersion === latestVersion) {
|
|
147
|
+
this.logger.debug('wakatime-cli is up to date');
|
|
148
|
+
callback(true);
|
|
149
|
+
}
|
|
150
|
+
else if (latestVersion) {
|
|
151
|
+
this.logger.debug(`Found an updated wakatime-cli ${latestVersion}`);
|
|
152
|
+
callback(false);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
this.logger.debug('Unable to find latest wakatime-cli version');
|
|
156
|
+
callback(false);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
callback(false);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
callback(false);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
getLatestCliVersion(callback) {
|
|
170
|
+
const proxy = this.options.getSetting('settings', 'proxy');
|
|
171
|
+
const noSSLVerify = this.options.getSetting('settings', 'no_ssl_verify');
|
|
172
|
+
let options = {
|
|
173
|
+
url: this.githubReleasesUrl,
|
|
174
|
+
json: true,
|
|
175
|
+
headers: {
|
|
176
|
+
'User-Agent': 'github.com/wakatime/vscode-wakatime',
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
this.logger.debug(`Fetching latest wakatime-cli version from GitHub API: ${options.url}`);
|
|
180
|
+
if (proxy) {
|
|
181
|
+
this.logger.debug(`Using Proxy: ${proxy}`);
|
|
182
|
+
options['proxy'] = proxy;
|
|
183
|
+
}
|
|
184
|
+
if (noSSLVerify === 'true')
|
|
185
|
+
options['strictSSL'] = false;
|
|
186
|
+
try {
|
|
187
|
+
request.get(options, (error, response, json) => {
|
|
188
|
+
if (!error && response && response.statusCode == 200) {
|
|
189
|
+
this.logger.debug(`GitHub API Response ${response.statusCode}`);
|
|
190
|
+
const latestCliVersion = json['tag_name'];
|
|
191
|
+
this.logger.debug(`Latest wakatime-cli version from GitHub: ${latestCliVersion}`);
|
|
192
|
+
this.options.setSetting('internal', 'cli_version_last_accessed', String(Math.round(Date.now() / 1000)), true);
|
|
193
|
+
callback(latestCliVersion);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
if (response) {
|
|
197
|
+
this.logger.warn(`GitHub API Response ${response.statusCode}: ${error}`);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
this.logger.warn(`GitHub API Response Error: ${error}`);
|
|
201
|
+
}
|
|
202
|
+
callback('');
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
catch (e) {
|
|
207
|
+
this.logger.warnException(e);
|
|
208
|
+
callback('');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
installCli(callback) {
|
|
212
|
+
this.logger.debug(`Downloading wakatime-cli from GitHub...`);
|
|
213
|
+
const url = this.cliDownloadUrl();
|
|
214
|
+
let zipFile = path.join(this.resourcesLocation, 'wakatime-cli' + this.randStr() + '.zip');
|
|
215
|
+
this.downloadFile(url, zipFile, () => {
|
|
216
|
+
this.extractCli(zipFile, callback);
|
|
217
|
+
}, callback);
|
|
218
|
+
}
|
|
219
|
+
isSymlink(file) {
|
|
220
|
+
try {
|
|
221
|
+
return fs.lstatSync(file).isSymbolicLink();
|
|
222
|
+
}
|
|
223
|
+
catch (_) { }
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
extractCli(zipFile, callback) {
|
|
227
|
+
this.logger.debug(`Extracting wakatime-cli into "${this.resourcesLocation}"...`);
|
|
228
|
+
this.backupCli();
|
|
229
|
+
this.unzip(zipFile, this.resourcesLocation, (unzipped) => {
|
|
230
|
+
if (!unzipped) {
|
|
231
|
+
this.restoreCli();
|
|
232
|
+
}
|
|
233
|
+
else if (!utils_1.Utils.isWindows()) {
|
|
234
|
+
this.removeCli();
|
|
235
|
+
const cli = this.getCliLocation();
|
|
236
|
+
try {
|
|
237
|
+
this.logger.debug('Chmod 755 wakatime-cli...');
|
|
238
|
+
fs.chmodSync(cli, 0o755);
|
|
239
|
+
}
|
|
240
|
+
catch (e) {
|
|
241
|
+
this.logger.warnException(e);
|
|
242
|
+
}
|
|
243
|
+
const ext = utils_1.Utils.isWindows() ? '.exe' : '';
|
|
244
|
+
const link = path.join(this.resourcesLocation, `wakatime-cli${ext}`);
|
|
245
|
+
if (!this.isSymlink(link)) {
|
|
246
|
+
try {
|
|
247
|
+
this.logger.debug(`Create symlink from wakatime-cli to ${cli}`);
|
|
248
|
+
fs.symlinkSync(cli, link);
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
this.logger.warnException(e);
|
|
252
|
+
try {
|
|
253
|
+
fs.copyFileSync(cli, link);
|
|
254
|
+
fs.chmodSync(link, 0o755);
|
|
255
|
+
}
|
|
256
|
+
catch (e2) {
|
|
257
|
+
this.logger.warnException(e2);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
callback();
|
|
263
|
+
});
|
|
264
|
+
this.logger.debug('Finished extracting wakatime-cli.');
|
|
265
|
+
}
|
|
266
|
+
backupCli() {
|
|
267
|
+
if (fs.existsSync(this.getCliLocation())) {
|
|
268
|
+
fs.renameSync(this.getCliLocation(), `${this.getCliLocation()}.backup`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
restoreCli() {
|
|
272
|
+
const backup = `${this.getCliLocation()}.backup`;
|
|
273
|
+
if (fs.existsSync(backup)) {
|
|
274
|
+
fs.renameSync(backup, this.getCliLocation());
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
removeCli() {
|
|
278
|
+
const backup = `${this.getCliLocation()}.backup`;
|
|
279
|
+
if (fs.existsSync(backup)) {
|
|
280
|
+
fs.unlinkSync(backup);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
downloadFile(url, outputFile, callback, error) {
|
|
284
|
+
const proxy = this.options.getSetting('settings', 'proxy');
|
|
285
|
+
const noSSLVerify = this.options.getSetting('settings', 'no_ssl_verify');
|
|
286
|
+
let options = { url: url };
|
|
287
|
+
if (proxy) {
|
|
288
|
+
this.logger.debug(`Using Proxy: ${proxy}`);
|
|
289
|
+
options['proxy'] = proxy;
|
|
290
|
+
}
|
|
291
|
+
if (noSSLVerify === 'true')
|
|
292
|
+
options['strictSSL'] = false;
|
|
293
|
+
try {
|
|
294
|
+
let r = request.get(options);
|
|
295
|
+
r.on('error', (e) => {
|
|
296
|
+
this.logger.warn(`Failed to download ${url}`);
|
|
297
|
+
this.logger.warn(e.toString());
|
|
298
|
+
error();
|
|
299
|
+
});
|
|
300
|
+
let out = fs.createWriteStream(outputFile);
|
|
301
|
+
r.pipe(out);
|
|
302
|
+
r.on('end', () => {
|
|
303
|
+
out.on('finish', () => {
|
|
304
|
+
callback();
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
catch (e) {
|
|
309
|
+
this.logger.warnException(e);
|
|
310
|
+
callback();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
unzip(file, outputDir, callback) {
|
|
314
|
+
if (fs.existsSync(file)) {
|
|
315
|
+
try {
|
|
316
|
+
let zip = new adm_zip_1.default(file);
|
|
317
|
+
zip.extractAllTo(outputDir, true);
|
|
318
|
+
fs.unlinkSync(file);
|
|
319
|
+
callback(true);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
catch (e) {
|
|
323
|
+
this.logger.warnException(e);
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
fs.unlinkSync(file);
|
|
327
|
+
}
|
|
328
|
+
catch (e2) {
|
|
329
|
+
this.logger.warnException(e2);
|
|
330
|
+
}
|
|
331
|
+
callback(false);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
legacyReleaseTag() {
|
|
335
|
+
const osname = this.osName();
|
|
336
|
+
const legacyOS = this.legacyOperatingSystems[osname];
|
|
337
|
+
if (!legacyOS)
|
|
338
|
+
return;
|
|
339
|
+
const version = legacyOS.find((spec) => {
|
|
340
|
+
try {
|
|
341
|
+
return semver.lt(os.release(), spec.kernelLessThan);
|
|
342
|
+
}
|
|
343
|
+
catch (e) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
return version?.tag;
|
|
348
|
+
}
|
|
349
|
+
architecture() {
|
|
350
|
+
const arch = os.arch();
|
|
351
|
+
if (arch.indexOf('32') > -1)
|
|
352
|
+
return '386';
|
|
353
|
+
if (arch.indexOf('x64') > -1)
|
|
354
|
+
return 'amd64';
|
|
355
|
+
return arch;
|
|
356
|
+
}
|
|
357
|
+
osName() {
|
|
358
|
+
let osname = os.platform();
|
|
359
|
+
if (osname == 'win32')
|
|
360
|
+
osname = 'windows';
|
|
361
|
+
return osname;
|
|
362
|
+
}
|
|
363
|
+
cliDownloadUrl() {
|
|
364
|
+
const osname = this.osName();
|
|
365
|
+
const arch = this.architecture();
|
|
366
|
+
// Use legacy wakatime-cli release to support older operating systems
|
|
367
|
+
const tag = this.legacyReleaseTag();
|
|
368
|
+
if (tag) {
|
|
369
|
+
return `https://github.com/wakatime/wakatime-cli/releases/download/${tag}/wakatime-cli-${osname}-${arch}.zip`;
|
|
370
|
+
}
|
|
371
|
+
const validCombinations = [
|
|
372
|
+
'android-amd64',
|
|
373
|
+
'android-arm64',
|
|
374
|
+
'darwin-amd64',
|
|
375
|
+
'darwin-arm64',
|
|
376
|
+
'freebsd-386',
|
|
377
|
+
'freebsd-amd64',
|
|
378
|
+
'freebsd-arm',
|
|
379
|
+
'linux-386',
|
|
380
|
+
'linux-amd64',
|
|
381
|
+
'linux-arm',
|
|
382
|
+
'linux-arm64',
|
|
383
|
+
'netbsd-386',
|
|
384
|
+
'netbsd-amd64',
|
|
385
|
+
'netbsd-arm',
|
|
386
|
+
'openbsd-386',
|
|
387
|
+
'openbsd-amd64',
|
|
388
|
+
'openbsd-arm',
|
|
389
|
+
'openbsd-arm64',
|
|
390
|
+
'windows-386',
|
|
391
|
+
'windows-amd64',
|
|
392
|
+
'windows-arm64',
|
|
393
|
+
];
|
|
394
|
+
if (!validCombinations.includes(`${osname}-${arch}`))
|
|
395
|
+
this.reportMissingPlatformSupport(osname, arch);
|
|
396
|
+
return `${this.githubDownloadUrl}/wakatime-cli-${osname}-${arch}.zip`;
|
|
397
|
+
}
|
|
398
|
+
reportMissingPlatformSupport(osname, architecture) {
|
|
399
|
+
const url = `https://api.wakatime.com/api/v1/cli-missing?osname=${osname}&architecture=${architecture}&plugin=vscode`;
|
|
400
|
+
const proxy = this.options.getSetting('settings', 'proxy');
|
|
401
|
+
const noSSLVerify = this.options.getSetting('settings', 'no_ssl_verify');
|
|
402
|
+
let options = { url: url };
|
|
403
|
+
if (proxy)
|
|
404
|
+
options['proxy'] = proxy;
|
|
405
|
+
if (noSSLVerify === 'true')
|
|
406
|
+
options['strictSSL'] = false;
|
|
407
|
+
try {
|
|
408
|
+
request.get(options);
|
|
409
|
+
}
|
|
410
|
+
catch (e) { }
|
|
411
|
+
}
|
|
412
|
+
randStr() {
|
|
413
|
+
return (Math.random() + 1).toString(36).substring(7);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
exports.Dependencies = Dependencies;
|
package/dist/index.js
CHANGED
|
@@ -9,17 +9,19 @@ const path_1 = __importDefault(require("path"));
|
|
|
9
9
|
const os_1 = __importDefault(require("os"));
|
|
10
10
|
const child_process_1 = require("child_process");
|
|
11
11
|
const options_1 = require("./options");
|
|
12
|
-
const
|
|
12
|
+
const version_1 = require("./version");
|
|
13
|
+
const dependencies_1 = require("./dependencies");
|
|
14
|
+
const utils_1 = require("./utils");
|
|
15
|
+
const logger_1 = require("./logger");
|
|
13
16
|
const STATE_FILE = path_1.default.join(os_1.default.homedir(), '.wakatime', 'claude-code.json');
|
|
14
|
-
const SESSION_LOG_FILE = path_1.default.join(os_1.default.homedir(), '.wakatime', 'claude-sessions.log');
|
|
15
17
|
const WAKATIME_CLI = path_1.default.join(os_1.default.homedir(), '.wakatime', 'wakatime-cli');
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
function shouldSendHeartbeat(inp) {
|
|
19
|
+
if (inp?.hook_event_name === 'Stop') {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
20
22
|
try {
|
|
21
|
-
const last = JSON.parse(fs_1.default.readFileSync(STATE_FILE, 'utf-8')).lastHeartbeatAt ?? timestamp();
|
|
22
|
-
return timestamp() - last >= 60;
|
|
23
|
+
const last = JSON.parse(fs_1.default.readFileSync(STATE_FILE, 'utf-8')).lastHeartbeatAt ?? utils_1.Utils.timestamp();
|
|
24
|
+
return utils_1.Utils.timestamp() - last >= 60;
|
|
23
25
|
}
|
|
24
26
|
catch {
|
|
25
27
|
return true;
|
|
@@ -38,20 +40,57 @@ function parseInput() {
|
|
|
38
40
|
}
|
|
39
41
|
return undefined;
|
|
40
42
|
}
|
|
41
|
-
function
|
|
43
|
+
function getLastHeartbeat() {
|
|
42
44
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
const stateData = JSON.parse(fs_1.default.readFileSync(STATE_FILE, 'utf-8'));
|
|
46
|
+
return stateData.lastHeartbeatAt ?? 0;
|
|
45
47
|
}
|
|
46
|
-
catch
|
|
47
|
-
|
|
48
|
+
catch {
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function calculateLineChanges(transcriptPath) {
|
|
53
|
+
try {
|
|
54
|
+
if (!transcriptPath || !fs_1.default.existsSync(transcriptPath)) {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
const content = fs_1.default.readFileSync(transcriptPath, 'utf-8');
|
|
58
|
+
const lines = content.split('\n');
|
|
59
|
+
let totalLineChanges = 0;
|
|
60
|
+
const lastHeartbeatAt = getLastHeartbeat();
|
|
61
|
+
for (const line of lines) {
|
|
62
|
+
if (line.trim()) {
|
|
63
|
+
try {
|
|
64
|
+
const logEntry = JSON.parse(line);
|
|
65
|
+
// Only count changes since last heartbeat
|
|
66
|
+
if (logEntry.timestamp && logEntry.toolUseResult?.structuredPatch) {
|
|
67
|
+
const entryTimestamp = new Date(logEntry.timestamp).getTime() / 1000;
|
|
68
|
+
if (entryTimestamp >= lastHeartbeatAt) {
|
|
69
|
+
const patches = logEntry.toolUseResult.structuredPatch;
|
|
70
|
+
for (const patch of patches) {
|
|
71
|
+
if (patch.newLines !== undefined && patch.oldLines !== undefined) {
|
|
72
|
+
totalLineChanges += patch.newLines - patch.oldLines;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// ignore
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return totalLineChanges;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return 0;
|
|
48
87
|
}
|
|
49
88
|
}
|
|
50
89
|
function updateState() {
|
|
51
90
|
fs_1.default.mkdirSync(path_1.default.dirname(STATE_FILE), { recursive: true });
|
|
52
|
-
fs_1.default.writeFileSync(STATE_FILE, JSON.stringify({ lastHeartbeatAt: timestamp() }, null, 2));
|
|
91
|
+
fs_1.default.writeFileSync(STATE_FILE, JSON.stringify({ lastHeartbeatAt: utils_1.Utils.timestamp() }, null, 2));
|
|
53
92
|
}
|
|
54
|
-
function sendHeartbeat(inp) {
|
|
93
|
+
function sendHeartbeat(inp, logger) {
|
|
55
94
|
const projectFolder = inp?.cwd;
|
|
56
95
|
try {
|
|
57
96
|
const args = [
|
|
@@ -62,26 +101,44 @@ function sendHeartbeat(inp) {
|
|
|
62
101
|
'--category',
|
|
63
102
|
'ai coding',
|
|
64
103
|
'--plugin',
|
|
65
|
-
`claude-code-wakatime/${VERSION}`,
|
|
104
|
+
`claude-code-wakatime/${version_1.VERSION}`,
|
|
66
105
|
];
|
|
67
106
|
if (projectFolder) {
|
|
68
107
|
args.push('--project-folder');
|
|
69
108
|
args.push(projectFolder);
|
|
70
109
|
}
|
|
110
|
+
if (inp?.transcript_path) {
|
|
111
|
+
const lineChanges = calculateLineChanges(inp.transcript_path);
|
|
112
|
+
if (lineChanges !== 0) {
|
|
113
|
+
args.push('--ai-line-changes');
|
|
114
|
+
args.push(lineChanges.toString());
|
|
115
|
+
}
|
|
116
|
+
}
|
|
71
117
|
(0, child_process_1.execFileSync)(WAKATIME_CLI, args);
|
|
72
118
|
}
|
|
73
119
|
catch (err) {
|
|
74
|
-
|
|
120
|
+
logger.errorException(err);
|
|
75
121
|
}
|
|
76
122
|
}
|
|
77
123
|
function main() {
|
|
78
124
|
const inp = parseInput();
|
|
79
125
|
const options = new options_1.Options();
|
|
80
126
|
const debug = options.getSetting('settings', 'debug');
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
|
|
127
|
+
const logger = new logger_1.Logger(debug === 'true' ? logger_1.LogLevel.DEBUG : logger_1.LogLevel.INFO);
|
|
128
|
+
const deps = new dependencies_1.Dependencies(options, logger);
|
|
129
|
+
if (inp) {
|
|
130
|
+
try {
|
|
131
|
+
logger.debug(JSON.stringify(inp, null, 2));
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
// ignore
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (inp?.hook_event_name === 'SessionStart') {
|
|
138
|
+
deps.checkAndInstallCli();
|
|
139
|
+
}
|
|
140
|
+
if (shouldSendHeartbeat(inp)) {
|
|
141
|
+
sendHeartbeat(inp, logger);
|
|
85
142
|
updateState();
|
|
86
143
|
}
|
|
87
144
|
}
|
package/dist/install-hooks.js
CHANGED
|
@@ -7,7 +7,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const os_1 = __importDefault(require("os"));
|
|
9
9
|
const CLAUDE_SETTINGS = path_1.default.join(os_1.default.homedir(), '.claude', 'settings.json');
|
|
10
|
-
const HOOK_EVENTS = ['PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'SessionStart'];
|
|
10
|
+
const HOOK_EVENTS = ['PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'SessionStart', 'Stop'];
|
|
11
11
|
function loadSettings() {
|
|
12
12
|
if (!fs_1.default.existsSync(CLAUDE_SETTINGS)) {
|
|
13
13
|
return {};
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Logger = exports.LogLevel = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const utils_1 = require("./utils");
|
|
11
|
+
var LogLevel;
|
|
12
|
+
(function (LogLevel) {
|
|
13
|
+
LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
|
|
14
|
+
LogLevel[LogLevel["INFO"] = 1] = "INFO";
|
|
15
|
+
LogLevel[LogLevel["WARN"] = 2] = "WARN";
|
|
16
|
+
LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
|
|
17
|
+
})(LogLevel || (exports.LogLevel = LogLevel = {}));
|
|
18
|
+
const LOG_FILE = path_1.default.join(os_1.default.homedir(), '.wakatime', 'claude-code.log');
|
|
19
|
+
class Logger {
|
|
20
|
+
constructor(level) {
|
|
21
|
+
this.level = LogLevel.INFO;
|
|
22
|
+
if (level)
|
|
23
|
+
this.setLevel(level);
|
|
24
|
+
}
|
|
25
|
+
getLevel() {
|
|
26
|
+
return this.level;
|
|
27
|
+
}
|
|
28
|
+
setLevel(level) {
|
|
29
|
+
this.level = level;
|
|
30
|
+
}
|
|
31
|
+
log(level, msg) {
|
|
32
|
+
if (level >= this.level) {
|
|
33
|
+
msg = `[${utils_1.Utils.timestamp()}][${LogLevel[level]}] ${msg}\n`;
|
|
34
|
+
fs_1.default.mkdirSync(path_1.default.dirname(LOG_FILE), { recursive: true });
|
|
35
|
+
fs_1.default.appendFileSync(LOG_FILE, msg);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
debug(msg) {
|
|
39
|
+
this.log(LogLevel.DEBUG, msg);
|
|
40
|
+
}
|
|
41
|
+
debugException(msg) {
|
|
42
|
+
if (msg.message !== undefined) {
|
|
43
|
+
this.log(LogLevel.DEBUG, msg.message);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
this.log(LogLevel.DEBUG, msg.toString());
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
info(msg) {
|
|
50
|
+
this.log(LogLevel.INFO, msg);
|
|
51
|
+
}
|
|
52
|
+
warn(msg) {
|
|
53
|
+
this.log(LogLevel.WARN, msg);
|
|
54
|
+
}
|
|
55
|
+
warnException(msg) {
|
|
56
|
+
if (msg.message !== undefined) {
|
|
57
|
+
this.log(LogLevel.WARN, msg.message);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
this.log(LogLevel.WARN, msg.toString());
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
error(msg) {
|
|
64
|
+
this.log(LogLevel.ERROR, msg);
|
|
65
|
+
}
|
|
66
|
+
errorException(msg) {
|
|
67
|
+
if (msg.message !== undefined) {
|
|
68
|
+
this.log(LogLevel.ERROR, msg.message);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
this.log(LogLevel.ERROR, msg.toString());
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
exports.Logger = Logger;
|
package/dist/utils.js
CHANGED