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 +8 -1
- package/dist/index.js +73 -7
- package/dist/install-hooks.js +1 -1
- package/dist/logger.js +2 -3
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/index.ts +23 -9
- package/src/install-hooks.ts +9 -10
- package/src/logger.ts +2 -2
package/README.md
CHANGED
|
@@ -6,12 +6,19 @@
|
|
|
6
6
|
|
|
7
7
|
1. `npm install -g claude-code-wakatime`
|
|
8
8
|
|
|
9
|
-
2.
|
|
9
|
+
2. Add your [api key](https://wakatime.com/settings/api-key) to `~/.wakatime.cfg`:
|
|
10
|
+
|
|
11
|
+
```
|
|
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
|

|
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
|
-
|
|
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
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
|
150
|
+
if (shouldSendHeartbeat(inp)) {
|
|
151
|
+
sendHeartbeat(inp);
|
|
86
152
|
updateState();
|
|
87
153
|
}
|
|
88
154
|
}
|
package/dist/install-hooks.js
CHANGED
|
@@ -7,7 +7,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
8
|
const os_1 = __importDefault(require("os"));
|
|
9
9
|
const CLAUDE_SETTINGS = path_1.default.join(os_1.default.homedir(), '.claude', 'settings.json');
|
|
10
|
-
const HOOK_EVENTS = ['PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'SessionStart'];
|
|
10
|
+
const HOOK_EVENTS = ['PreToolUse', 'PostToolUse', 'UserPromptSubmit', 'SessionStart', 'Stop'];
|
|
11
11
|
function loadSettings() {
|
|
12
12
|
if (!fs_1.default.existsSync(CLAUDE_SETTINGS)) {
|
|
13
13
|
return {};
|
package/dist/logger.js
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 = `[${
|
|
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
package/package.json
CHANGED
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 {
|
|
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
|
|
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
|
|
128
|
+
if (lineChanges) {
|
|
124
129
|
args.push('--ai-line-changes');
|
|
125
130
|
args.push(lineChanges.toString());
|
|
126
131
|
}
|
|
127
132
|
}
|
|
128
133
|
|
|
129
|
-
|
|
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
|
-
|
|
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
|
|
169
|
+
if (shouldSendHeartbeat(inp)) {
|
|
170
|
+
sendHeartbeat(inp);
|
|
157
171
|
updateState();
|
|
158
172
|
}
|
|
159
173
|
}
|
package/src/install-hooks.ts
CHANGED
|
@@ -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(
|
|
42
|
-
existingHook
|
|
43
|
-
|
|
44
|
-
|
|
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 = `[${
|
|
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
|
}
|