claude-code-wakatime 2.0.0 → 2.1.1

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 CHANGED
@@ -6,12 +6,19 @@
6
6
 
7
7
  1. `npm install -g claude-code-wakatime`
8
8
 
9
- 2. Install WakaTime for [VS Code](https://wakatime.com/vs-code) to setup your `~/.wakatime/` folder and api key.
9
+ 2. Add your [api key](https://wakatime.com/settings/api-key) to `~/.wakatime.cfg`:
10
+
11
+ ```
12
+ [settings]
13
+ api_key = waka_123
14
+ ```
10
15
 
11
16
  4. Use Claude Code and your AI coding activity will be displayed on your [WakaTime dashboard](https://wakatime.com)
12
17
 
13
18
  ## Usage
14
19
 
20
+ New: See the lines of code generated by AI on your dashboard!
21
+
15
22
  Visit [https://wakatime.com](https://wakatime.com) to see your coding activity.
16
23
 
17
24
  ![Project Overview](https://wakatime.com/static/img/ScreenShots/Screen-Shot-2016-03-21.png)
package/dist/index.js CHANGED
@@ -15,7 +15,11 @@ const utils_1 = require("./utils");
15
15
  const logger_1 = require("./logger");
16
16
  const STATE_FILE = path_1.default.join(os_1.default.homedir(), '.wakatime', 'claude-code.json');
17
17
  const WAKATIME_CLI = path_1.default.join(os_1.default.homedir(), '.wakatime', 'wakatime-cli');
18
- function shouldSendHeartbeat() {
18
+ const logger = new logger_1.Logger();
19
+ function shouldSendHeartbeat(inp) {
20
+ if (inp?.hook_event_name === 'Stop') {
21
+ return true;
22
+ }
19
23
  try {
20
24
  const last = JSON.parse(fs_1.default.readFileSync(STATE_FILE, 'utf-8')).lastHeartbeatAt ?? utils_1.Utils.timestamp();
21
25
  return utils_1.Utils.timestamp() - last >= 60;
@@ -37,11 +41,57 @@ function parseInput() {
37
41
  }
38
42
  return undefined;
39
43
  }
44
+ function getLastHeartbeat() {
45
+ try {
46
+ const stateData = JSON.parse(fs_1.default.readFileSync(STATE_FILE, 'utf-8'));
47
+ return stateData.lastHeartbeatAt ?? 0;
48
+ }
49
+ catch {
50
+ return 0;
51
+ }
52
+ }
53
+ function calculateLineChanges(transcriptPath) {
54
+ try {
55
+ if (!transcriptPath || !fs_1.default.existsSync(transcriptPath)) {
56
+ return 0;
57
+ }
58
+ const content = fs_1.default.readFileSync(transcriptPath, 'utf-8');
59
+ const lines = content.split('\n');
60
+ let totalLineChanges = 0;
61
+ const lastHeartbeatAt = getLastHeartbeat();
62
+ for (const line of lines) {
63
+ if (line.trim()) {
64
+ try {
65
+ const logEntry = JSON.parse(line);
66
+ // Only count changes since last heartbeat
67
+ if (logEntry.timestamp && logEntry.toolUseResult?.structuredPatch) {
68
+ const entryTimestamp = new Date(logEntry.timestamp).getTime() / 1000;
69
+ if (entryTimestamp >= lastHeartbeatAt) {
70
+ const patches = logEntry.toolUseResult.structuredPatch;
71
+ for (const patch of patches) {
72
+ if (patch.newLines !== undefined && patch.oldLines !== undefined) {
73
+ totalLineChanges += patch.newLines - patch.oldLines;
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+ catch {
80
+ // ignore
81
+ }
82
+ }
83
+ }
84
+ return totalLineChanges;
85
+ }
86
+ catch {
87
+ return 0;
88
+ }
89
+ }
40
90
  function updateState() {
41
91
  fs_1.default.mkdirSync(path_1.default.dirname(STATE_FILE), { recursive: true });
42
92
  fs_1.default.writeFileSync(STATE_FILE, JSON.stringify({ lastHeartbeatAt: utils_1.Utils.timestamp() }, null, 2));
43
93
  }
44
- function sendHeartbeat(inp, logger) {
94
+ function sendHeartbeat(inp) {
45
95
  const projectFolder = inp?.cwd;
46
96
  try {
47
97
  const args = [
@@ -58,7 +108,23 @@ function sendHeartbeat(inp, logger) {
58
108
  args.push('--project-folder');
59
109
  args.push(projectFolder);
60
110
  }
61
- (0, child_process_1.execFileSync)(WAKATIME_CLI, args);
111
+ if (inp?.transcript_path) {
112
+ const lineChanges = calculateLineChanges(inp.transcript_path);
113
+ if (lineChanges) {
114
+ args.push('--ai-line-changes');
115
+ args.push(lineChanges.toString());
116
+ }
117
+ }
118
+ const options = utils_1.Utils.buildOptions();
119
+ (0, child_process_1.execFile)(WAKATIME_CLI, args, options, (error, _stdout, stderr) => {
120
+ const output = _stdout.toString().trim() + stderr.toString().trim();
121
+ if (output) {
122
+ logger.error(output);
123
+ }
124
+ if (!(error != null)) {
125
+ logger.debug(`Sending heartbeat: ${args}`);
126
+ }
127
+ });
62
128
  }
63
129
  catch (err) {
64
130
  logger.errorException(err);
@@ -68,7 +134,7 @@ function main() {
68
134
  const inp = parseInput();
69
135
  const options = new options_1.Options();
70
136
  const debug = options.getSetting('settings', 'debug');
71
- const logger = new logger_1.Logger(debug === 'true' ? logger_1.LogLevel.DEBUG : logger_1.LogLevel.INFO);
137
+ logger.setLevel(debug === 'true' ? logger_1.LogLevel.DEBUG : logger_1.LogLevel.INFO);
72
138
  const deps = new dependencies_1.Dependencies(options, logger);
73
139
  if (inp) {
74
140
  try {
@@ -78,11 +144,11 @@ function main() {
78
144
  // ignore
79
145
  }
80
146
  }
81
- if (inp?.hook_event_name === 'SessionStart') {
147
+ if (inp?.hook_event_name === 'SessionStart demo') {
82
148
  deps.checkAndInstallCli();
83
149
  }
84
- if (shouldSendHeartbeat()) {
85
- sendHeartbeat(inp, logger);
150
+ if (shouldSendHeartbeat(inp)) {
151
+ sendHeartbeat(inp);
86
152
  updateState();
87
153
  }
88
154
  }
@@ -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 CHANGED
@@ -7,7 +7,6 @@ exports.Logger = exports.LogLevel = void 0;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const os_1 = __importDefault(require("os"));
10
- const utils_1 = require("./utils");
11
10
  var LogLevel;
12
11
  (function (LogLevel) {
13
12
  LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
@@ -19,7 +18,7 @@ const LOG_FILE = path_1.default.join(os_1.default.homedir(), '.wakatime', 'claud
19
18
  class Logger {
20
19
  constructor(level) {
21
20
  this.level = LogLevel.INFO;
22
- if (level)
21
+ if (level !== undefined)
23
22
  this.setLevel(level);
24
23
  }
25
24
  getLevel() {
@@ -30,7 +29,7 @@ class Logger {
30
29
  }
31
30
  log(level, msg) {
32
31
  if (level >= this.level) {
33
- msg = `[${utils_1.Utils.timestamp()}][${LogLevel[level]}] ${msg}\n`;
32
+ msg = `[${new Date().toISOString()}][${LogLevel[level]}] ${msg}\n`;
34
33
  fs_1.default.mkdirSync(path_1.default.dirname(LOG_FILE), { recursive: true });
35
34
  fs_1.default.appendFileSync(LOG_FILE, msg);
36
35
  }
package/dist/version.js CHANGED
@@ -2,4 +2,4 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VERSION = void 0;
4
4
  // This file is auto-generated during build. Do not edit manually.
5
- exports.VERSION = '1.0.1';
5
+ exports.VERSION = '2.1.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-wakatime",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "description": "WakaTime plugin for Claude Code",
5
5
  "bin": {
6
6
  "claude-code-wakatime": "dist/index.js"
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import fs from 'fs';
4
4
  import path from 'path';
5
5
  import os from 'os';
6
- import { execFileSync } from 'child_process';
6
+ import { execFile } from 'child_process';
7
7
  import { Options } from './options';
8
8
  import { VERSION } from './version';
9
9
  import { Dependencies } from './dependencies';
@@ -12,6 +12,7 @@ import { Logger, LogLevel } from './logger';
12
12
 
13
13
  const STATE_FILE = path.join(os.homedir(), '.wakatime', 'claude-code.json');
14
14
  const WAKATIME_CLI = path.join(os.homedir(), '.wakatime', 'wakatime-cli');
15
+ const logger = new Logger();
15
16
 
16
17
  type State = {
17
18
  lastHeartbeatAt?: number;
@@ -24,7 +25,11 @@ type Input = {
24
25
  hook_event_name: string;
25
26
  };
26
27
 
27
- function shouldSendHeartbeat(): boolean {
28
+ function shouldSendHeartbeat(inp?: Input): boolean {
29
+ if (inp?.hook_event_name === 'Stop') {
30
+ return true;
31
+ }
32
+
28
33
  try {
29
34
  const last = (JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8')) as State).lastHeartbeatAt ?? Utils.timestamp();
30
35
  return Utils.timestamp() - last >= 60;
@@ -100,7 +105,7 @@ function updateState() {
100
105
  fs.writeFileSync(STATE_FILE, JSON.stringify({ lastHeartbeatAt: Utils.timestamp() } as State, null, 2));
101
106
  }
102
107
 
103
- function sendHeartbeat(inp: Input | undefined, logger: Logger) {
108
+ function sendHeartbeat(inp: Input | undefined) {
104
109
  const projectFolder = inp?.cwd;
105
110
  try {
106
111
  const args: string[] = [
@@ -120,13 +125,22 @@ function sendHeartbeat(inp: Input | undefined, logger: Logger) {
120
125
 
121
126
  if (inp?.transcript_path) {
122
127
  const lineChanges = calculateLineChanges(inp.transcript_path);
123
- if (lineChanges !== 0) {
128
+ if (lineChanges) {
124
129
  args.push('--ai-line-changes');
125
130
  args.push(lineChanges.toString());
126
131
  }
127
132
  }
128
133
 
129
- execFileSync(WAKATIME_CLI, args);
134
+ const options = Utils.buildOptions();
135
+ execFile(WAKATIME_CLI, args, options, (error, _stdout, stderr) => {
136
+ const output = _stdout.toString().trim() + stderr.toString().trim();
137
+ if (output) {
138
+ logger.error(output);
139
+ }
140
+ if (!(error != null)) {
141
+ logger.debug(`Sending heartbeat: ${args}`);
142
+ }
143
+ });
130
144
  } catch (err: any) {
131
145
  logger.errorException(err);
132
146
  }
@@ -137,7 +151,7 @@ function main() {
137
151
 
138
152
  const options = new Options();
139
153
  const debug = options.getSetting('settings', 'debug');
140
- const logger = new Logger(debug === 'true' ? LogLevel.DEBUG : LogLevel.INFO);
154
+ logger.setLevel(debug === 'true' ? LogLevel.DEBUG : LogLevel.INFO);
141
155
  const deps = new Dependencies(options, logger);
142
156
 
143
157
  if (inp) {
@@ -148,12 +162,12 @@ function main() {
148
162
  }
149
163
  }
150
164
 
151
- if (inp?.hook_event_name === 'SessionStart') {
165
+ if (inp?.hook_event_name === 'SessionStart demo') {
152
166
  deps.checkAndInstallCli();
153
167
  }
154
168
 
155
- if (shouldSendHeartbeat()) {
156
- sendHeartbeat(inp, logger);
169
+ if (shouldSendHeartbeat(inp)) {
170
+ sendHeartbeat(inp);
157
171
  updateState();
158
172
  }
159
173
  }
@@ -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 CHANGED
@@ -16,7 +16,7 @@ export class Logger {
16
16
  private level: LogLevel = LogLevel.INFO;
17
17
 
18
18
  constructor(level?: LogLevel) {
19
- if (level) this.setLevel(level);
19
+ if (level !== undefined) this.setLevel(level);
20
20
  }
21
21
 
22
22
  public getLevel(): LogLevel {
@@ -29,7 +29,7 @@ export class Logger {
29
29
 
30
30
  public log(level: LogLevel, msg: string): void {
31
31
  if (level >= this.level) {
32
- msg = `[${Utils.timestamp()}][${LogLevel[level]}] ${msg}\n`;
32
+ msg = `[${new Date().toISOString()}][${LogLevel[level]}] ${msg}\n`;
33
33
  fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
34
34
  fs.appendFileSync(LOG_FILE, msg);
35
35
  }