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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-wakatime",
3
- "version": "1.0.1",
3
+ "version": "2.1.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,14 @@ type Input = {
21
24
  hook_event_name: string;
22
25
  };
23
26
 
24
- function timestamp() {
25
- return Date.now() / 1000;
26
- }
27
+ function shouldSendHeartbeat(inp?: Input): boolean {
28
+ if (inp?.hook_event_name === 'Stop') {
29
+ return true;
30
+ }
27
31
 
28
- function shouldSendHeartbeat(): boolean {
29
32
  try {
30
- const last = (JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8')) as State).lastHeartbeatAt ?? timestamp();
31
- return timestamp() - last >= 60;
33
+ const last = (JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8')) as State).lastHeartbeatAt ?? Utils.timestamp();
34
+ return Utils.timestamp() - last >= 60;
32
35
  } catch {
33
36
  return true;
34
37
  }
@@ -47,21 +50,61 @@ function parseInput() {
47
50
  return undefined;
48
51
  }
49
52
 
50
- function logSessionData(inp: Input) {
53
+ function getLastHeartbeat() {
51
54
  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
55
+ const stateData = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8')) as State;
56
+ return stateData.lastHeartbeatAt ?? 0;
57
+ } catch {
58
+ return 0;
59
+ }
60
+ }
61
+
62
+ function calculateLineChanges(transcriptPath: string): number {
63
+ try {
64
+ if (!transcriptPath || !fs.existsSync(transcriptPath)) {
65
+ return 0;
66
+ }
67
+
68
+ const content = fs.readFileSync(transcriptPath, 'utf-8');
69
+ const lines = content.split('\n');
70
+ let totalLineChanges = 0;
71
+
72
+ const lastHeartbeatAt = getLastHeartbeat();
73
+ for (const line of lines) {
74
+ if (line.trim()) {
75
+ try {
76
+ const logEntry = JSON.parse(line);
77
+
78
+ // Only count changes since last heartbeat
79
+ if (logEntry.timestamp && logEntry.toolUseResult?.structuredPatch) {
80
+ const entryTimestamp = new Date(logEntry.timestamp).getTime() / 1000;
81
+ if (entryTimestamp >= lastHeartbeatAt) {
82
+ const patches = logEntry.toolUseResult.structuredPatch;
83
+ for (const patch of patches) {
84
+ if (patch.newLines !== undefined && patch.oldLines !== undefined) {
85
+ totalLineChanges += patch.newLines - patch.oldLines;
86
+ }
87
+ }
88
+ }
89
+ }
90
+ } catch {
91
+ // ignore
92
+ }
93
+ }
94
+ }
95
+
96
+ return totalLineChanges;
97
+ } catch {
98
+ return 0;
56
99
  }
57
100
  }
58
101
 
59
102
  function updateState() {
60
103
  fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
61
- fs.writeFileSync(STATE_FILE, JSON.stringify({ lastHeartbeatAt: timestamp() } as State, null, 2));
104
+ fs.writeFileSync(STATE_FILE, JSON.stringify({ lastHeartbeatAt: Utils.timestamp() } as State, null, 2));
62
105
  }
63
106
 
64
- function sendHeartbeat(inp?: Input) {
107
+ function sendHeartbeat(inp: Input | undefined, logger: Logger) {
65
108
  const projectFolder = inp?.cwd;
66
109
  try {
67
110
  const args: string[] = [
@@ -78,9 +121,18 @@ function sendHeartbeat(inp?: Input) {
78
121
  args.push('--project-folder');
79
122
  args.push(projectFolder);
80
123
  }
124
+
125
+ if (inp?.transcript_path) {
126
+ const lineChanges = calculateLineChanges(inp.transcript_path);
127
+ if (lineChanges !== 0) {
128
+ args.push('--ai-line-changes');
129
+ args.push(lineChanges.toString());
130
+ }
131
+ }
132
+
81
133
  execFileSync(WAKATIME_CLI, args);
82
134
  } catch (err: any) {
83
- console.error('Failed to send WakaTime heartbeat:', err.message);
135
+ logger.errorException(err);
84
136
  }
85
137
  }
86
138
 
@@ -89,11 +141,23 @@ function main() {
89
141
 
90
142
  const options = new Options();
91
143
  const debug = options.getSetting('settings', 'debug');
144
+ const logger = new Logger(debug === 'true' ? LogLevel.DEBUG : LogLevel.INFO);
145
+ const deps = new Dependencies(options, logger);
146
+
147
+ if (inp) {
148
+ try {
149
+ logger.debug(JSON.stringify(inp, null, 2));
150
+ } catch (err) {
151
+ // ignore
152
+ }
153
+ }
92
154
 
93
- if (inp && debug === 'true') logSessionData(inp);
155
+ if (inp?.hook_event_name === 'SessionStart') {
156
+ deps.checkAndInstallCli();
157
+ }
94
158
 
95
- if (shouldSendHeartbeat()) {
96
- sendHeartbeat(inp);
159
+ if (shouldSendHeartbeat(inp)) {
160
+ sendHeartbeat(inp, logger);
97
161
  updateState();
98
162
  }
99
163
  }
@@ -3,7 +3,7 @@ import path from 'path';
3
3
  import os from 'os';
4
4
 
5
5
  const CLAUDE_SETTINGS = path.join(os.homedir(), '.claude', 'settings.json');
6
- const HOOK_EVENTS = ['PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'SessionStart'];
6
+ const HOOK_EVENTS = ['PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'SessionStart', 'Stop'];
7
7
 
8
8
  function loadSettings(): any {
9
9
  if (!fs.existsSync(CLAUDE_SETTINGS)) {
@@ -33,19 +33,18 @@ function installHooks(): void {
33
33
  };
34
34
 
35
35
  let hookAlreadyExists = true;
36
-
36
+
37
37
  for (const event of HOOK_EVENTS) {
38
38
  settings.hooks[event] = settings.hooks[event] || [];
39
-
39
+
40
40
  // Check if a hook with the same command already exists
41
- const existingHook = settings.hooks[event].find((existingHook: any) =>
42
- existingHook.hooks &&
43
- Array.isArray(existingHook.hooks) &&
44
- existingHook.hooks.some((hookItem: any) =>
45
- hookItem.command === 'claude-code-wakatime'
46
- )
41
+ const existingHook = settings.hooks[event].find(
42
+ (existingHook: any) =>
43
+ existingHook.hooks &&
44
+ Array.isArray(existingHook.hooks) &&
45
+ existingHook.hooks.some((hookItem: any) => hookItem.command === 'claude-code-wakatime'),
47
46
  );
48
-
47
+
49
48
  if (!existingHook) {
50
49
  settings.hooks[event].push(hook);
51
50
  hookAlreadyExists = false;
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();