opencode-codetime 0.4.1 → 0.5.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/README.md +49 -3
- package/dist/codetime.d.ts +22 -0
- package/dist/codetime.js +74 -0
- package/dist/index.js +56 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,13 +8,17 @@
|
|
|
8
8
|
|
|
9
9
|
[CodeTime](https://codetime.dev) plugin for [OpenCode](https://github.com/anomalyco/opencode) -- track your AI coding activity and time spent.
|
|
10
10
|
|
|
11
|
+
<img width="885" height="312" alt="image" src="https://github.com/user-attachments/assets/5f15a838-af86-4d4c-ab07-a9e467779bca" />
|
|
12
|
+
|
|
11
13
|
## Features
|
|
12
14
|
|
|
13
15
|
- **Automatic time tracking** -- sends coding events to CodeTime when OpenCode reads, edits, or writes files
|
|
14
16
|
- **Check your coding time** -- ask the AI "what's my coding time?" and it fetches your stats via the `codetime` tool
|
|
17
|
+
- **Per-project filtering** -- view coding time for the current project or any specific project
|
|
18
|
+
- **Project breakdown** -- see a ranked table of all projects with time spent today
|
|
15
19
|
- **Language detection** -- detects 90+ programming languages from file extensions
|
|
16
20
|
- **Git integration** -- captures current branch and remote origin
|
|
17
|
-
- **Project identification** -- shows as `[opencode]
|
|
21
|
+
- **Project identification** -- shows as `directory-name [opencode]` on your CodeTime dashboard
|
|
18
22
|
- **Rate-limited** -- one heartbeat per 2 minutes to avoid API spam
|
|
19
23
|
- **Session lifecycle** -- flushes pending events on session end so no data is lost
|
|
20
24
|
- **Zero config** -- just set your token and go
|
|
@@ -89,7 +93,7 @@ Each heartbeat sent to CodeTime includes:
|
|
|
89
93
|
|-------|-------|
|
|
90
94
|
| `eventTime` | Unix timestamp of the event |
|
|
91
95
|
| `language` | Detected from file extension (e.g. `TypeScript`, `Python`) |
|
|
92
|
-
| `project` | `[opencode]
|
|
96
|
+
| `project` | `directory-name [opencode]` |
|
|
93
97
|
| `relativeFile` | File path relative to the project root |
|
|
94
98
|
| `editor` | `opencode` |
|
|
95
99
|
| `platform` | OS platform (`darwin`, `linux`, `windows`) |
|
|
@@ -104,6 +108,48 @@ Each heartbeat sent to CodeTime includes:
|
|
|
104
108
|
| `chat.message` | Processes pending heartbeats on chat activity |
|
|
105
109
|
| `tool` | Registers `codetime` tool to check today's coding time |
|
|
106
110
|
|
|
111
|
+
### Commands
|
|
112
|
+
|
|
113
|
+
| Command | Description |
|
|
114
|
+
|---------|-------------|
|
|
115
|
+
| `/codetime` | Show today's total coding time |
|
|
116
|
+
| `/codetime-breakdown` | Show today's coding time broken down by project |
|
|
117
|
+
|
|
118
|
+
### `codetime` tool
|
|
119
|
+
|
|
120
|
+
The `codetime` tool supports optional arguments for project filtering:
|
|
121
|
+
|
|
122
|
+
| Argument | Type | Description |
|
|
123
|
+
|----------|------|-------------|
|
|
124
|
+
| `project` | string (optional) | Filter by project name. Use `"current"` to auto-detect the current project. Omit to show total time. |
|
|
125
|
+
| `breakdown` | boolean (optional) | When `true`, show a ranked breakdown of all projects. |
|
|
126
|
+
|
|
127
|
+
**Usage examples** (in natural language to the AI):
|
|
128
|
+
|
|
129
|
+
- "What's my coding time?" -- shows total time across all projects
|
|
130
|
+
- "How long have I been coding on this project?" -- shows time for the current project
|
|
131
|
+
- "Show me a breakdown of my coding time by project" -- shows ranked project list
|
|
132
|
+
- "How much time did I spend on my-app today?" -- shows time for a specific project
|
|
133
|
+
|
|
134
|
+
**Example outputs:**
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
# Total time (default)
|
|
138
|
+
Today's coding time: 2h 42m
|
|
139
|
+
|
|
140
|
+
# Current project
|
|
141
|
+
Today's coding time for opencode-codetime [opencode]: 1h 23m (Total across all projects: 2h 42m)
|
|
142
|
+
|
|
143
|
+
# Project breakdown
|
|
144
|
+
Today's coding time by project:
|
|
145
|
+
|
|
146
|
+
opencode-codetime [opencode] 1h 23m
|
|
147
|
+
my-other-project [vscode] 52m
|
|
148
|
+
side-project [opencode] 27m
|
|
149
|
+
──────────────────────────────────────
|
|
150
|
+
Total 2h 42m
|
|
151
|
+
```
|
|
152
|
+
|
|
107
153
|
### Tool tracking
|
|
108
154
|
|
|
109
155
|
| Tool | Data Extracted |
|
|
@@ -119,7 +165,7 @@ Each heartbeat sent to CodeTime includes:
|
|
|
119
165
|
```
|
|
120
166
|
src/
|
|
121
167
|
index.ts Main plugin entry point with event hooks
|
|
122
|
-
codetime.ts CodeTime API client (token validation, heartbeats)
|
|
168
|
+
codetime.ts CodeTime API client (token validation, heartbeats, stats)
|
|
123
169
|
state.ts Rate limiter (max 1 heartbeat per 2 minutes)
|
|
124
170
|
language.ts File extension to language name mapping
|
|
125
171
|
git.ts Git branch and remote origin extraction
|
package/dist/codetime.d.ts
CHANGED
|
@@ -28,3 +28,25 @@ export declare function sendHeartbeat(token: string, event: EventLogRequest): Pr
|
|
|
28
28
|
* Fetch today's coding minutes from CodeTime API.
|
|
29
29
|
*/
|
|
30
30
|
export declare function getTodayMinutes(token: string): Promise<number | null>;
|
|
31
|
+
export interface StatsTimeData {
|
|
32
|
+
duration: number;
|
|
33
|
+
time: string;
|
|
34
|
+
}
|
|
35
|
+
export interface StatsTimeResponse {
|
|
36
|
+
data: StatsTimeData[];
|
|
37
|
+
}
|
|
38
|
+
export interface TopEntry {
|
|
39
|
+
field: string;
|
|
40
|
+
minutes: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Fetch coding minutes for a specific project using the stats_time endpoint.
|
|
44
|
+
* Returns total minutes for the project within the last 24 hours,
|
|
45
|
+
* or a custom time range if start/end are provided.
|
|
46
|
+
*/
|
|
47
|
+
export declare function getProjectMinutes(token: string, project: string): Promise<number | null>;
|
|
48
|
+
/**
|
|
49
|
+
* Fetch top projects by coding time using the top endpoint.
|
|
50
|
+
* Returns a ranked list of projects with their minutes.
|
|
51
|
+
*/
|
|
52
|
+
export declare function getTopProjects(token: string, minutes?: number): Promise<TopEntry[] | null>;
|
package/dist/codetime.js
CHANGED
|
@@ -83,3 +83,77 @@ export async function getTodayMinutes(token) {
|
|
|
83
83
|
return null;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Fetch coding minutes for a specific project using the stats_time endpoint.
|
|
88
|
+
* Returns total minutes for the project within the last 24 hours,
|
|
89
|
+
* or a custom time range if start/end are provided.
|
|
90
|
+
*/
|
|
91
|
+
export async function getProjectMinutes(token, project) {
|
|
92
|
+
try {
|
|
93
|
+
const params = new URLSearchParams({
|
|
94
|
+
project,
|
|
95
|
+
unit: "minutes",
|
|
96
|
+
limit: "1440",
|
|
97
|
+
});
|
|
98
|
+
const response = await fetch(`${API_BASE}/v3/users/self/stats_time?${params.toString()}`, {
|
|
99
|
+
method: "GET",
|
|
100
|
+
headers: {
|
|
101
|
+
Authorization: `Bearer ${token}`,
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
await logger.warn("Failed to fetch project minutes", {
|
|
107
|
+
status: response.status,
|
|
108
|
+
statusText: response.statusText,
|
|
109
|
+
project,
|
|
110
|
+
});
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const data = (await response.json());
|
|
114
|
+
// Sum all duration values from the response
|
|
115
|
+
const totalMinutes = data.data.reduce((sum, entry) => sum + entry.duration, 0);
|
|
116
|
+
return totalMinutes;
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
await logger.error("Project minutes request failed", {
|
|
120
|
+
error: String(err),
|
|
121
|
+
project,
|
|
122
|
+
});
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Fetch top projects by coding time using the top endpoint.
|
|
128
|
+
* Returns a ranked list of projects with their minutes.
|
|
129
|
+
*/
|
|
130
|
+
export async function getTopProjects(token, minutes = 1440) {
|
|
131
|
+
try {
|
|
132
|
+
const params = new URLSearchParams({
|
|
133
|
+
field: "workspace",
|
|
134
|
+
minutes: String(minutes),
|
|
135
|
+
});
|
|
136
|
+
const response = await fetch(`${API_BASE}/v3/users/self/top?${params.toString()}`, {
|
|
137
|
+
method: "GET",
|
|
138
|
+
headers: {
|
|
139
|
+
Authorization: `Bearer ${token}`,
|
|
140
|
+
"Content-Type": "application/json",
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
await logger.warn("Failed to fetch top projects", {
|
|
145
|
+
status: response.status,
|
|
146
|
+
statusText: response.statusText,
|
|
147
|
+
});
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const data = (await response.json());
|
|
151
|
+
return data;
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
await logger.error("Top projects request failed", {
|
|
155
|
+
error: String(err),
|
|
156
|
+
});
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as os from "node:os";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
4
|
-
import { sendHeartbeat, validateToken, getTodayMinutes, } from "./codetime.js";
|
|
4
|
+
import { sendHeartbeat, validateToken, getTodayMinutes, getProjectMinutes, getTopProjects, } from "./codetime.js";
|
|
5
5
|
import { getGitBranch, getGitOrigin } from "./git.js";
|
|
6
6
|
import { detectLanguage } from "./language.js";
|
|
7
7
|
import { initLogger, info, warn, error, debug } from "./logger.js";
|
|
@@ -261,18 +261,70 @@ export const plugin = async (ctx) => {
|
|
|
261
261
|
"Immediately call `codetime` with no arguments and return its output verbatim.\n" +
|
|
262
262
|
"Do not call other tools.",
|
|
263
263
|
};
|
|
264
|
+
cfg.command["codetime-breakdown"] = {
|
|
265
|
+
description: "Show today's coding time breakdown by project",
|
|
266
|
+
template: "Retrieve CodeTime coding time stats broken down by project.\n\n" +
|
|
267
|
+
'Immediately call `codetime` with `breakdown: true` and return its output verbatim.\n' +
|
|
268
|
+
"Do not call other tools.",
|
|
269
|
+
};
|
|
264
270
|
},
|
|
265
271
|
tool: {
|
|
266
272
|
codetime: tool({
|
|
267
273
|
description: "Show today's coding time tracked by CodeTime. " +
|
|
268
274
|
"Use this when the user asks about their coding time, " +
|
|
269
|
-
"how long they've been coding, or wants to see their CodeTime stats."
|
|
270
|
-
|
|
271
|
-
|
|
275
|
+
"how long they've been coding, or wants to see their CodeTime stats. " +
|
|
276
|
+
"Supports filtering by project name and showing a breakdown of time across all projects.",
|
|
277
|
+
args: {
|
|
278
|
+
project: tool.schema.string().optional().describe("Filter by project name. Use 'current' to auto-detect the current project. " +
|
|
279
|
+
"Omit to show total time across all projects."),
|
|
280
|
+
breakdown: tool.schema.boolean().optional().describe("When true, show a breakdown of coding time across all projects today."),
|
|
281
|
+
},
|
|
282
|
+
async execute(args) {
|
|
272
283
|
if (!_token) {
|
|
273
284
|
return "CodeTime is not configured. Set CODETIME_TOKEN environment variable to enable tracking. Get your token from https://codetime.dev/dashboard/settings";
|
|
274
285
|
}
|
|
275
286
|
try {
|
|
287
|
+
// Breakdown mode: show all projects ranked by time
|
|
288
|
+
if (args.breakdown) {
|
|
289
|
+
const projects = await getTopProjects(_token);
|
|
290
|
+
if (projects === null || projects.length === 0) {
|
|
291
|
+
return "No project data available for today.";
|
|
292
|
+
}
|
|
293
|
+
// Calculate total
|
|
294
|
+
const totalMinutes = projects.reduce((sum, p) => sum + p.minutes, 0);
|
|
295
|
+
// Find the longest project name for alignment
|
|
296
|
+
const maxNameLen = Math.max(...projects.map((p) => p.field.length), "Total".length);
|
|
297
|
+
const lines = ["Today's coding time by project:", ""];
|
|
298
|
+
for (const p of projects) {
|
|
299
|
+
const name = p.field.padEnd(maxNameLen + 2);
|
|
300
|
+
lines.push(` ${name}${formatMinutes(p.minutes)}`);
|
|
301
|
+
}
|
|
302
|
+
lines.push(` ${"─".repeat(maxNameLen + 2 + 8)}`);
|
|
303
|
+
lines.push(` ${"Total".padEnd(maxNameLen + 2)}${formatMinutes(totalMinutes)}`);
|
|
304
|
+
return lines.join("\n");
|
|
305
|
+
}
|
|
306
|
+
// Project-specific mode
|
|
307
|
+
const projectName = args.project === "current" ? _projectName : args.project;
|
|
308
|
+
if (projectName) {
|
|
309
|
+
// Fetch both project-specific and total in parallel
|
|
310
|
+
const [projectMins, totalMins] = await Promise.all([
|
|
311
|
+
getProjectMinutes(_token, projectName),
|
|
312
|
+
getTodayMinutes(_token),
|
|
313
|
+
]);
|
|
314
|
+
if (projectMins === null) {
|
|
315
|
+
return `Failed to fetch coding time for project "${projectName}" from CodeTime API.`;
|
|
316
|
+
}
|
|
317
|
+
const projectFormatted = formatMinutes(projectMins);
|
|
318
|
+
const displayName = args.project === "current"
|
|
319
|
+
? _projectName
|
|
320
|
+
: projectName;
|
|
321
|
+
if (totalMins !== null) {
|
|
322
|
+
const totalFormatted = formatMinutes(totalMins);
|
|
323
|
+
return `Today's coding time for ${displayName}: ${projectFormatted} (Total across all projects: ${totalFormatted})`;
|
|
324
|
+
}
|
|
325
|
+
return `Today's coding time for ${displayName}: ${projectFormatted}`;
|
|
326
|
+
}
|
|
327
|
+
// Default: total coding time (original behavior)
|
|
276
328
|
const minutes = await getTodayMinutes(_token);
|
|
277
329
|
if (minutes === null) {
|
|
278
330
|
return "Failed to fetch coding time from CodeTime API.";
|