claude-code-wakatime 3.0.5 → 3.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/.claude-plugin/marketplace.json +6 -3
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +2 -8
- package/dist/index.js +190 -183
- package/hooks/hooks.json +8 -8
- package/package.json +4 -4
- package/scripts/generate-version.js +15 -3
- package/scripts/run +24 -0
- package/src/index.ts +12 -12
- package/src/types.ts +20 -1
- package/src/utils.ts +23 -14
package/hooks/hooks.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "
|
|
9
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/run"
|
|
10
10
|
}
|
|
11
11
|
]
|
|
12
12
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"hooks": [
|
|
18
18
|
{
|
|
19
19
|
"type": "command",
|
|
20
|
-
"command": "
|
|
20
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/run"
|
|
21
21
|
}
|
|
22
22
|
]
|
|
23
23
|
}
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"hooks": [
|
|
29
29
|
{
|
|
30
30
|
"type": "command",
|
|
31
|
-
"command": "
|
|
31
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/run"
|
|
32
32
|
}
|
|
33
33
|
]
|
|
34
34
|
}
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"hooks": [
|
|
40
40
|
{
|
|
41
41
|
"type": "command",
|
|
42
|
-
"command": "
|
|
42
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/run"
|
|
43
43
|
}
|
|
44
44
|
]
|
|
45
45
|
}
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"hooks": [
|
|
51
51
|
{
|
|
52
52
|
"type": "command",
|
|
53
|
-
"command": "
|
|
53
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/run"
|
|
54
54
|
}
|
|
55
55
|
]
|
|
56
56
|
}
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"hooks": [
|
|
62
62
|
{
|
|
63
63
|
"type": "command",
|
|
64
|
-
"command": "
|
|
64
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/run"
|
|
65
65
|
}
|
|
66
66
|
]
|
|
67
67
|
}
|
|
@@ -72,10 +72,10 @@
|
|
|
72
72
|
"hooks": [
|
|
73
73
|
{
|
|
74
74
|
"type": "command",
|
|
75
|
-
"command": "
|
|
75
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/run"
|
|
76
76
|
}
|
|
77
77
|
]
|
|
78
78
|
}
|
|
79
79
|
]
|
|
80
80
|
}
|
|
81
|
-
}
|
|
81
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-wakatime",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"description": "WakaTime plugin for Claude Code",
|
|
5
5
|
"bin": {
|
|
6
6
|
"claude-code-wakatime": "dist/index.js"
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
"build:legacy": "esbuild src/install-hooks.ts --bundle --platform=node --outfile=dist/install-hooks.js",
|
|
12
12
|
"build": "npm run build:legacy && esbuild src/index.ts --bundle --platform=node --outfile=dist/index.js",
|
|
13
13
|
"watch": "npm run prebuild && tsc --watch",
|
|
14
|
-
"release:major": "npm version major && npm run build && npm publish && git push && git push --tags",
|
|
15
|
-
"release:minor": "npm version minor && npm run build && npm publish && git push && git push --tags",
|
|
16
|
-
"release:patch": "npm version patch && npm run build && npm publish && git push && git push --tags"
|
|
14
|
+
"release:major": "npm version major && npm run build && npm publish && git commit -am \"build release\" && git push && git push --tags",
|
|
15
|
+
"release:minor": "npm version minor && npm run build && npm publish && git commit -am \"build release\" && git push && git push --tags",
|
|
16
|
+
"release:patch": "npm version patch && npm run build && npm publish && git commit -am \"build release\" && git push && git push --tags"
|
|
17
17
|
},
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|
|
@@ -5,18 +5,30 @@ const path = require('path');
|
|
|
5
5
|
|
|
6
6
|
const packagePath = path.join(__dirname, '..', 'package.json');
|
|
7
7
|
const versionFilePath = path.join(__dirname, '..', 'src', 'version.ts');
|
|
8
|
+
const marketplaceFilePath = path.join(__dirname, '..', '.claude-plugin', 'marketplace.json');
|
|
9
|
+
const pluginFilePath = path.join(__dirname, '..', '.claude-plugin', 'plugin.json');
|
|
8
10
|
|
|
9
11
|
try {
|
|
10
12
|
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
11
13
|
const version = packageJson.version;
|
|
12
|
-
|
|
14
|
+
|
|
13
15
|
const versionFileContent = `// This file is auto-generated during build. Do not edit manually.
|
|
14
16
|
export const VERSION = '${version}';
|
|
15
17
|
`;
|
|
16
|
-
|
|
18
|
+
|
|
17
19
|
fs.writeFileSync(versionFilePath, versionFileContent);
|
|
18
20
|
console.log(`Generated version.ts with version ${version}`);
|
|
21
|
+
|
|
22
|
+
const marketplace = JSON.parse(fs.readFileSync(marketplaceFilePath, 'utf8'));
|
|
23
|
+
marketplace.plugins[0].version = version;
|
|
24
|
+
fs.writeFileSync(marketplaceFilePath, JSON.stringify(marketplace, null, 2));
|
|
25
|
+
console.log(`Generated marketplace.json with version ${version}`);
|
|
26
|
+
|
|
27
|
+
const plugin = JSON.parse(fs.readFileSync(pluginFilePath, 'utf8'));
|
|
28
|
+
plugin.version = version;
|
|
29
|
+
fs.writeFileSync(pluginFilePath, JSON.stringify(plugin, null, 2));
|
|
30
|
+
console.log(`Generated plugin.json with version ${version}`);
|
|
19
31
|
} catch (error) {
|
|
20
32
|
console.error('Error generating version file:', error);
|
|
21
33
|
process.exit(1);
|
|
22
|
-
}
|
|
34
|
+
}
|
package/scripts/run
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
ROOT="${CLAUDE_PLUGIN_ROOT:-$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)}"
|
|
5
|
+
|
|
6
|
+
# If user provides an explicit node path, use it
|
|
7
|
+
if [ -n "${NODE_BIN:-}" ] && [ -x "${NODE_BIN}" ]; then
|
|
8
|
+
exec "${NODE_BIN}" "$ROOT/dist/index.js" "$@"
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
# If node exists anyway, use it
|
|
12
|
+
if command -v node >/dev/null 2>&1; then
|
|
13
|
+
exec node "$ROOT/dist/index.js" "$@"
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# NixOS bootstrap: run node from nixpkgs (flakes)
|
|
17
|
+
if command -v nix >/dev/null 2>&1; then
|
|
18
|
+
exec nix run nixpkgs#nodejs -- node "$ROOT/dist/index.js" "$@"
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
echo "Error: Node.js not found. On NixOS either:
|
|
22
|
+
- set NODE_BIN=/absolute/path/to/node, or
|
|
23
|
+
- ensure 'nix' is available and flakes are enabled." >&2
|
|
24
|
+
exit 127
|
package/src/index.ts
CHANGED
|
@@ -4,27 +4,27 @@ import { execFile } from 'child_process';
|
|
|
4
4
|
import { Options } from './options';
|
|
5
5
|
import { VERSION } from './version';
|
|
6
6
|
import { Dependencies } from './dependencies';
|
|
7
|
-
import { logger,
|
|
7
|
+
import { logger, LogLevel } from './logger';
|
|
8
8
|
import { Input } from './types';
|
|
9
9
|
import { buildOptions, formatArguments, getEntityFiles, parseInput, shouldSendHeartbeat, updateState } from './utils';
|
|
10
10
|
|
|
11
11
|
const options = new Options();
|
|
12
12
|
const deps = new Dependencies(options, logger);
|
|
13
13
|
|
|
14
|
-
function sendHeartbeat(inp: Input | undefined): boolean {
|
|
14
|
+
async function sendHeartbeat(inp: Input | undefined): Promise<boolean> {
|
|
15
15
|
const projectFolder = inp?.cwd;
|
|
16
|
-
const { entities, claudeVersion } = getEntityFiles(inp);
|
|
16
|
+
const { entities, claudeVersion } = await getEntityFiles(inp);
|
|
17
17
|
if (entities.size === 0) return false;
|
|
18
18
|
|
|
19
19
|
const wakatime_cli = deps.getCliLocation();
|
|
20
20
|
|
|
21
|
-
for (const [
|
|
22
|
-
logger.debug(`Entity: ${
|
|
21
|
+
for (const [entityFile, entityData] of entities.entries()) {
|
|
22
|
+
logger.debug(`Entity: ${entityFile}`);
|
|
23
23
|
const args: string[] = [
|
|
24
24
|
'--entity',
|
|
25
|
-
|
|
25
|
+
entityFile,
|
|
26
26
|
'--entity-type',
|
|
27
|
-
|
|
27
|
+
entityData.type,
|
|
28
28
|
'--category',
|
|
29
29
|
'ai coding',
|
|
30
30
|
'--plugin',
|
|
@@ -35,9 +35,9 @@ function sendHeartbeat(inp: Input | undefined): boolean {
|
|
|
35
35
|
args.push(projectFolder);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
if (lineChanges) {
|
|
38
|
+
if (entityData.lineChanges) {
|
|
39
39
|
args.push('--ai-line-changes');
|
|
40
|
-
args.push(lineChanges.toString());
|
|
40
|
+
args.push(entityData.lineChanges.toString());
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
logger.debug(`Sending heartbeat: ${formatArguments(wakatime_cli, args)}`);
|
|
@@ -53,7 +53,7 @@ function sendHeartbeat(inp: Input | undefined): boolean {
|
|
|
53
53
|
return true;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function main() {
|
|
56
|
+
async function main() {
|
|
57
57
|
const inp = parseInput();
|
|
58
58
|
|
|
59
59
|
const debug = options.getSetting('settings', 'debug');
|
|
@@ -65,8 +65,8 @@ function main() {
|
|
|
65
65
|
deps.checkAndInstallCli();
|
|
66
66
|
|
|
67
67
|
if (shouldSendHeartbeat(inp)) {
|
|
68
|
-
if (sendHeartbeat(inp)) {
|
|
69
|
-
updateState();
|
|
68
|
+
if (await sendHeartbeat(inp)) {
|
|
69
|
+
await updateState(inp);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
} catch (err) {
|
package/src/types.ts
CHANGED
|
@@ -2,11 +2,23 @@ export type State = {
|
|
|
2
2
|
lastHeartbeatAt?: number;
|
|
3
3
|
};
|
|
4
4
|
|
|
5
|
+
export type HookEvent =
|
|
6
|
+
| 'PreToolUse'
|
|
7
|
+
| 'PermissionRequest'
|
|
8
|
+
| 'PostToolUse'
|
|
9
|
+
| 'Notification'
|
|
10
|
+
| 'UserPromptSubmit'
|
|
11
|
+
| 'Stop'
|
|
12
|
+
| 'SubagentStop'
|
|
13
|
+
| 'PreCompact'
|
|
14
|
+
| 'SessionStart'
|
|
15
|
+
| 'SessionEnd';
|
|
16
|
+
|
|
5
17
|
export type Input = {
|
|
6
18
|
session_id: string;
|
|
7
19
|
transcript_path: string;
|
|
8
20
|
cwd: string;
|
|
9
|
-
hook_event_name:
|
|
21
|
+
hook_event_name: HookEvent;
|
|
10
22
|
};
|
|
11
23
|
|
|
12
24
|
export type TranscriptLog = {
|
|
@@ -48,3 +60,10 @@ export type TranscriptLog = {
|
|
|
48
60
|
replaceAll?: string;
|
|
49
61
|
};
|
|
50
62
|
};
|
|
63
|
+
|
|
64
|
+
export type Entity = {
|
|
65
|
+
type: 'file' | 'app';
|
|
66
|
+
lineChanges: number;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type EntityMap = Map<string, Entity>;
|
package/src/utils.ts
CHANGED
|
@@ -2,12 +2,9 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as os from 'os';
|
|
3
3
|
import * as child_process from 'child_process';
|
|
4
4
|
import { StdioOptions } from 'child_process';
|
|
5
|
-
import { Input, State, TranscriptLog } from './types';
|
|
6
|
-
import path from 'path';
|
|
5
|
+
import { Entity, EntityMap, Input, State, TranscriptLog } from './types';
|
|
7
6
|
import { logger } from './logger';
|
|
8
7
|
|
|
9
|
-
const STATE_FILE = path.join(os.homedir(), '.wakatime', 'claude-code.json');
|
|
10
|
-
|
|
11
8
|
export function parseInput() {
|
|
12
9
|
try {
|
|
13
10
|
const stdinData = fs.readFileSync(0, 'utf-8');
|
|
@@ -21,26 +18,33 @@ export function parseInput() {
|
|
|
21
18
|
return undefined;
|
|
22
19
|
}
|
|
23
20
|
|
|
21
|
+
function getStateFile(inp: Input): string {
|
|
22
|
+
return `${inp.transcript_path}.wakatime`;
|
|
23
|
+
}
|
|
24
|
+
|
|
24
25
|
export function shouldSendHeartbeat(inp?: Input): boolean {
|
|
25
26
|
if (inp?.hook_event_name === 'Stop') {
|
|
26
27
|
return true;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
if (!inp) return false;
|
|
31
|
+
|
|
29
32
|
try {
|
|
30
|
-
const last = (JSON.parse(fs.readFileSync(
|
|
33
|
+
const last = (JSON.parse(fs.readFileSync(getStateFile(inp), 'utf-8')) as State).lastHeartbeatAt ?? timestamp();
|
|
31
34
|
return timestamp() - last >= 60;
|
|
32
35
|
} catch {
|
|
33
36
|
return true;
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
export function updateState() {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
export async function updateState(inp?: Input) {
|
|
41
|
+
if (!inp) return;
|
|
42
|
+
const file = getStateFile(inp);
|
|
43
|
+
await fs.promises.writeFile(file, JSON.stringify({ lastHeartbeatAt: timestamp() } as State, null, 2));
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
export function getEntityFiles(inp: Input | undefined): { entities:
|
|
43
|
-
const entities = new Map<string,
|
|
46
|
+
export async function getEntityFiles(inp: Input | undefined): Promise<{ entities: EntityMap; claudeVersion: string }> {
|
|
47
|
+
const entities = new Map<string, Entity>() as EntityMap;
|
|
44
48
|
let claudeVersion = '';
|
|
45
49
|
|
|
46
50
|
const transcriptPath = inp?.transcript_path;
|
|
@@ -48,7 +52,7 @@ export function getEntityFiles(inp: Input | undefined): { entities: Map<string,
|
|
|
48
52
|
return { entities, claudeVersion };
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
const lastHeartbeatAt = getLastHeartbeat();
|
|
55
|
+
const lastHeartbeatAt = await getLastHeartbeat(inp);
|
|
52
56
|
|
|
53
57
|
const content = fs.readFileSync(transcriptPath, 'utf-8');
|
|
54
58
|
for (const logLine of content.split('\n')) {
|
|
@@ -71,12 +75,17 @@ export function getEntityFiles(inp: Input | undefined): { entities: Map<string,
|
|
|
71
75
|
|
|
72
76
|
const lineChanges = patches.map((patch) => patch.newLines - patch.oldLines).reduce((p, c) => p + c, 0);
|
|
73
77
|
|
|
74
|
-
|
|
78
|
+
const prevLineChanges = (entities.get(filePath) ?? ({ lineChanges: 0 } as Entity)).lineChanges;
|
|
79
|
+
entities.set(filePath, { lineChanges: prevLineChanges + lineChanges, type: 'file' });
|
|
75
80
|
} catch (err) {
|
|
76
81
|
logger.warnException(err);
|
|
77
82
|
}
|
|
78
83
|
}
|
|
79
84
|
|
|
85
|
+
if (inp.hook_event_name == 'UserPromptSubmit' && entities.size === 0) {
|
|
86
|
+
entities.set(inp.cwd, { lineChanges: 0, type: 'app' });
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
return { entities, claudeVersion };
|
|
81
90
|
}
|
|
82
91
|
|
|
@@ -116,9 +125,9 @@ export function buildOptions(stdin?: boolean): Object {
|
|
|
116
125
|
return options;
|
|
117
126
|
}
|
|
118
127
|
|
|
119
|
-
function getLastHeartbeat() {
|
|
128
|
+
async function getLastHeartbeat(inp: Input) {
|
|
120
129
|
try {
|
|
121
|
-
const stateData = JSON.parse(fs.
|
|
130
|
+
const stateData = JSON.parse(await fs.promises.readFile(getStateFile(inp), 'utf-8')) as State;
|
|
122
131
|
return stateData.lastHeartbeatAt ?? 0;
|
|
123
132
|
} catch {
|
|
124
133
|
return 0;
|