claude-code-wakatime 3.0.1 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+
25
+ // src/install-hooks.ts
26
+ var import_fs = __toESM(require("fs"));
27
+ var import_path = __toESM(require("path"));
28
+ var import_os = __toESM(require("os"));
29
+ var CLAUDE_SETTINGS = import_path.default.join(import_os.default.homedir(), ".claude", "settings.json");
30
+ var HOOK_EVENTS = ["PreToolUse", "PostToolUse", "SessionEnd", "UserPromptSubmit", "PreCompact", "SubagentStop", "Stop"];
31
+ function loadSettings() {
32
+ if (!import_fs.default.existsSync(CLAUDE_SETTINGS)) {
33
+ return {};
34
+ }
35
+ return JSON.parse(import_fs.default.readFileSync(CLAUDE_SETTINGS, "utf-8"));
36
+ }
37
+ function saveSettings(settings) {
38
+ import_fs.default.mkdirSync(import_path.default.dirname(CLAUDE_SETTINGS), { recursive: true });
39
+ import_fs.default.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2));
40
+ }
41
+ function installHooks() {
42
+ const settings = loadSettings();
43
+ settings.hooks = settings.hooks || {};
44
+ const hook = {
45
+ matcher: "*",
46
+ hooks: [
47
+ {
48
+ type: "command",
49
+ command: "claude-code-wakatime"
50
+ }
51
+ ]
52
+ };
53
+ let hookAlreadyExists = true;
54
+ for (const event of HOOK_EVENTS) {
55
+ settings.hooks[event] = settings.hooks[event] || [];
56
+ const existingHook = settings.hooks[event].find(
57
+ (existingHook2) => existingHook2.hooks && Array.isArray(existingHook2.hooks) && existingHook2.hooks.some((hookItem) => hookItem.command === "claude-code-wakatime")
58
+ );
59
+ if (!existingHook) {
60
+ settings.hooks[event].push(hook);
61
+ hookAlreadyExists = false;
62
+ }
63
+ }
64
+ if (hookAlreadyExists) {
65
+ console.log(`WakaTime hooks already installed in Claude ${CLAUDE_SETTINGS}`);
66
+ } else {
67
+ saveSettings(settings);
68
+ console.log(`WakaTime hooks installed in Claude ${CLAUDE_SETTINGS}`);
69
+ }
70
+ }
71
+ installHooks();
package/hooks/hooks.json CHANGED
@@ -1,16 +1,5 @@
1
1
  {
2
2
  "hooks": {
3
- "SessionStart": [
4
- {
5
- "matcher": "",
6
- "hooks": [
7
- {
8
- "type": "command",
9
- "command": "node ${CLAUDE_PLUGIN_ROOT}/dist/index.js"
10
- }
11
- ]
12
- }
13
- ],
14
3
  "SessionEnd": [
15
4
  {
16
5
  "matcher": "",
@@ -89,4 +78,4 @@
89
78
  }
90
79
  ]
91
80
  }
92
- }
81
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-wakatime",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "description": "WakaTime plugin for Claude Code",
5
5
  "bin": {
6
6
  "claude-code-wakatime": "dist/index.js"
@@ -8,7 +8,8 @@
8
8
  "scripts": {
9
9
  "postinstall": "node dist/install-hooks.js",
10
10
  "prebuild": "node scripts/generate-version.js",
11
- "build": "esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js",
11
+ "build:legacy": "esbuild src/install-hooks.ts --bundle --platform=node --outfile=dist/install-hooks.js",
12
+ "build": "npm run build:legacy && esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js",
12
13
  "watch": "npm run prebuild && tsc --watch",
13
14
  "release:major": "npm run build && npm version major && npm publish && git push && git push --tags",
14
15
  "release:minor": "npm run build && npm version minor && npm publish && git push && git push --tags",
@@ -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 = Utils.isWindows() ? '.exe' : '';
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${Utils.isWindows() ? '.exe' : ''}`;
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 = Utils.buildOptions();
99
+ const options = buildOptions();
100
100
  try {
101
101
  child_process.execFile(this.getCliLocation(), args, options, (error, _stdout, stderr) => {
102
- if (!(error != null)) {
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 (!Utils.isWindows()) {
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 = Utils.isWindows() ? '.exe' : '';
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
- callback();
291
+ error();
292
292
  }
293
293
  }
294
294
 
package/src/index.ts CHANGED
@@ -1,196 +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 { Utils } from './utils';
11
- import { Logger, LogLevel } from './logger';
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 WAKATIME_CLI = path.join(os.homedir(), '.wakatime', 'wakatime-cli');
15
- const logger = new Logger();
11
+ const options = new Options();
12
+ const deps = new Dependencies(options, logger);
16
13
 
17
- type State = {
18
- lastHeartbeatAt?: number;
19
- };
20
-
21
- type Input = {
22
- session_id: string;
23
- transcript_path: string;
24
- cwd: string;
25
- hook_event_name: string;
26
- };
27
-
28
- function shouldSendHeartbeat(inp?: Input): boolean {
29
- if (inp?.hook_event_name === 'Stop') {
30
- return true;
31
- }
32
-
33
- try {
34
- const last = (JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8')) as State).lastHeartbeatAt ?? Utils.timestamp();
35
- return Utils.timestamp() - last >= 60;
36
- } catch {
37
- return true;
38
- }
39
- }
40
-
41
- function parseInput() {
42
- try {
43
- const stdinData = fs.readFileSync(0, 'utf-8');
44
- if (stdinData.trim()) {
45
- const input: Input = JSON.parse(stdinData);
46
- return input;
47
- }
48
- } catch (err) {
49
- console.error(err);
50
- }
51
- return undefined;
52
- }
53
-
54
- function getLastHeartbeat() {
55
- try {
56
- const stateData = JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8')) as State;
57
- return stateData.lastHeartbeatAt ?? 0;
58
- } catch {
59
- return 0;
60
- }
61
- }
62
-
63
- function getModifiedFile(transcriptPath: string): string | undefined {
64
- try {
65
- if (!transcriptPath || !fs.existsSync(transcriptPath)) {
66
- return undefined;
67
- }
68
-
69
- const content = fs.readFileSync(transcriptPath, 'utf-8');
70
- const lines = content.split('\n');
71
- const fileLineChanges = new Map<string, number>();
72
-
73
- const lastHeartbeatAt = getLastHeartbeat();
74
- for (const line of lines) {
75
- if (line.trim()) {
76
- try {
77
- const logEntry = JSON.parse(line);
78
- if (!logEntry.timestamp) continue;
79
-
80
- const entryTimestamp = new Date(logEntry.timestamp).getTime() / 1000;
81
- if (entryTimestamp >= lastHeartbeatAt) {
82
- let filePath: string | undefined;
83
-
84
- // Check for file paths in tool use results
85
- if (logEntry.toolUse?.parameters?.file_path) {
86
- filePath = logEntry.toolUse.parameters.file_path;
87
- }
88
-
89
- // Check for file paths in tool use results for multi-edit
90
- if (logEntry.toolUse?.parameters?.edits) {
91
- filePath = logEntry.toolUse.parameters.file_path;
92
- }
93
-
94
- // Check for file paths and line changes in structured patch
95
- if (logEntry.toolUseResult?.structuredPatch) {
96
- const patches = logEntry.toolUseResult.structuredPatch;
97
- for (const patch of patches) {
98
- if (patch.file) {
99
- filePath = patch.file as string;
100
- if (patch.newLines !== undefined && patch.oldLines !== undefined) {
101
- const lineChanges = Math.abs(patch.newLines - patch.oldLines);
102
- fileLineChanges.set(filePath, (fileLineChanges.get(filePath) || 0) + lineChanges);
103
- }
104
- }
105
- }
106
- }
107
-
108
- if (filePath && !fileLineChanges.has(filePath)) {
109
- fileLineChanges.set(filePath, 0);
110
- }
111
- }
112
- } catch {
113
- // ignore
114
- }
115
- }
116
- }
117
-
118
- if (fileLineChanges.size === 0) {
119
- return undefined;
120
- }
121
-
122
- // Find file with most line changes
123
- let maxChanges = 0;
124
- let mostChangedFile: string | undefined;
125
- for (const [file, changes] of fileLineChanges.entries()) {
126
- if (changes > maxChanges) {
127
- maxChanges = changes;
128
- mostChangedFile = file;
129
- }
130
- }
131
-
132
- return mostChangedFile;
133
- } catch {
134
- return undefined;
135
- }
136
- }
137
-
138
- function calculateLineChanges(transcriptPath: string): number {
139
- try {
140
- if (!transcriptPath || !fs.existsSync(transcriptPath)) {
141
- return 0;
142
- }
143
-
144
- const content = fs.readFileSync(transcriptPath, 'utf-8');
145
- const lines = content.split('\n');
146
- let totalLineChanges = 0;
147
-
148
- const lastHeartbeatAt = getLastHeartbeat();
149
- for (const line of lines) {
150
- if (line.trim()) {
151
- try {
152
- const logEntry = JSON.parse(line);
153
-
154
- // Only count changes since last heartbeat
155
- if (logEntry.timestamp && logEntry.toolUseResult?.structuredPatch) {
156
- const entryTimestamp = new Date(logEntry.timestamp).getTime() / 1000;
157
- if (entryTimestamp >= lastHeartbeatAt) {
158
- const patches = logEntry.toolUseResult.structuredPatch;
159
- for (const patch of patches) {
160
- if (patch.newLines !== undefined && patch.oldLines !== undefined) {
161
- totalLineChanges += patch.newLines - patch.oldLines;
162
- }
163
- }
164
- }
165
- }
166
- } catch {
167
- // ignore
168
- }
169
- }
170
- }
171
-
172
- return totalLineChanges;
173
- } catch {
174
- return 0;
175
- }
176
- }
177
-
178
- function updateState() {
179
- fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
180
- fs.writeFileSync(STATE_FILE, JSON.stringify({ lastHeartbeatAt: Utils.timestamp() } as State, null, 2));
181
- }
182
-
183
- function getEntityFile(inp: Input | undefined): string | undefined {
184
- if (!inp?.transcript_path) return;
185
- return getModifiedFile(inp.transcript_path);
186
- }
187
-
188
- function sendHeartbeat(inp: Input | undefined) {
14
+ function sendHeartbeat(inp: Input | undefined): boolean {
189
15
  const projectFolder = inp?.cwd;
190
- try {
191
- const entity = getEntityFile(inp);
192
- if (!entity) return;
16
+ const { entities, claudeVersion } = getEntityFiles(inp);
17
+ if (entities.size === 0) return false;
193
18
 
19
+ const wakatime_cli = deps.getCliLocation();
20
+
21
+ for (const [entity, lineChanges] of entities.entries()) {
22
+ logger.debug(`Entity: ${entity}`);
194
23
  const args: string[] = [
195
24
  '--entity',
196
25
  entity,
@@ -199,57 +28,49 @@ function sendHeartbeat(inp: Input | undefined) {
199
28
  '--category',
200
29
  'ai coding',
201
30
  '--plugin',
202
- `claude-code-wakatime/${VERSION}`,
31
+ `claude/${claudeVersion} claude-code-wakatime/${VERSION}`,
203
32
  ];
204
33
  if (projectFolder) {
205
34
  args.push('--project-folder');
206
35
  args.push(projectFolder);
207
36
  }
208
37
 
209
- if (inp?.transcript_path) {
210
- const lineChanges = calculateLineChanges(inp.transcript_path);
211
- if (lineChanges) {
212
- args.push('--ai-line-changes');
213
- args.push(lineChanges.toString());
214
- }
38
+ if (lineChanges) {
39
+ args.push('--ai-line-changes');
40
+ args.push(lineChanges.toString());
215
41
  }
216
42
 
217
- logger.debug(`Sending heartbeat: ${args}`);
43
+ logger.debug(`Sending heartbeat: ${formatArguments(wakatime_cli, args)}`);
218
44
 
219
- const options = Utils.buildOptions();
220
- execFile(WAKATIME_CLI, args, options, (error, stdout, stderr) => {
45
+ const execOptions = buildOptions();
46
+ execFile(wakatime_cli, args, execOptions, (error, stdout, stderr) => {
221
47
  const output = stdout.toString().trim() + stderr.toString().trim();
222
48
  if (output) logger.error(output);
223
49
  if (error) logger.error(error.toString());
224
50
  });
225
- } catch (err: any) {
226
- logger.errorException(err);
227
51
  }
52
+
53
+ return true;
228
54
  }
229
55
 
230
56
  function main() {
231
57
  const inp = parseInput();
232
58
 
233
- const options = new Options();
234
59
  const debug = options.getSetting('settings', 'debug');
235
60
  logger.setLevel(debug === 'true' ? LogLevel.DEBUG : LogLevel.INFO);
236
- const deps = new Dependencies(options, logger);
237
61
 
238
- if (inp) {
239
- try {
240
- logger.debug(JSON.stringify(inp, null, 2));
241
- } catch (err) {
242
- // ignore
243
- }
244
- }
62
+ try {
63
+ if (inp) logger.debug(JSON.stringify(inp, null, 2));
245
64
 
246
- if (inp?.hook_event_name === 'SessionStart') {
247
65
  deps.checkAndInstallCli();
248
- }
249
66
 
250
- if (shouldSendHeartbeat(inp)) {
251
- sendHeartbeat(inp);
252
- updateState();
67
+ if (shouldSendHeartbeat(inp)) {
68
+ if (sendHeartbeat(inp)) {
69
+ updateState();
70
+ }
71
+ }
72
+ } catch (err) {
73
+ logger.errorException(err);
253
74
  }
254
75
  }
255
76
 
@@ -3,16 +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 = [
7
- 'PreToolUse',
8
- 'PostToolUse',
9
- 'SessionStart',
10
- 'SessionEnd',
11
- 'UserPromptSubmit',
12
- 'PreCompact',
13
- 'SubagentStop',
14
- 'Stop',
15
- ];
6
+ const HOOK_EVENTS = ['PreToolUse', 'PostToolUse', 'SessionEnd', 'UserPromptSubmit', 'PreCompact', 'SubagentStop', 'Stop'];
16
7
 
17
8
  function loadSettings(): any {
18
9
  if (!fs.existsSync(CLAUDE_SETTINGS)) {
package/src/logger.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
- import { Utils } from './utils';
5
4
 
6
5
  export enum LogLevel {
7
6
  DEBUG = 0,
@@ -75,3 +74,11 @@ export class Logger {
75
74
  }
76
75
  }
77
76
  }
77
+
78
+ const global = globalThis as unknown as {
79
+ logger: Logger | undefined;
80
+ };
81
+
82
+ export const logger = global.logger ?? new Logger();
83
+
84
+ global.logger = logger;
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 = Utils.getHomeDirectory();
19
+ const home = getHomeDirectory();
21
20
  const wakaFolder = path.join(home, '.wakatime');
22
21
  try {
23
22
  if (!fs.existsSync(wakaFolder)) {
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
+ };