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 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] project-name` on your CodeTime dashboard
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] directory-name` |
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
@@ -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
- args: {},
271
- async execute() {
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.";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-codetime",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "CodeTime plugin for OpenCode - Track AI coding activity and time spent with codetime.dev",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",