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