opencode-codetime 0.4.1 → 0.5.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
@@ -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";
@@ -257,22 +257,71 @@ export const plugin = async (ctx) => {
257
257
  cfg.command = cfg.command || {};
258
258
  cfg.command["codetime"] = {
259
259
  description: "Show today's coding time from CodeTime",
260
- template: "Retrieve current CodeTime coding time stats.\n\n" +
261
- "Immediately call `codetime` with no arguments and return its output verbatim.\n" +
262
- "Do not call other tools.",
260
+ template: "Retrieve CodeTime coding time stats.\n\n" +
261
+ "Based on the arguments provided: `$ARGUMENTS`\n\n" +
262
+ '- If no arguments (empty/blank), call `codetime` with `project: "current"` to show current project time.\n' +
263
+ "- If the argument is `breakdown`, call `codetime` with `breakdown: true` to show all projects.\n" +
264
+ "- Otherwise, call `codetime` with `project` set to the argument value to show that specific project's time.\n\n" +
265
+ "Return the output verbatim. Do not call other tools.",
263
266
  };
264
267
  },
265
268
  tool: {
266
269
  codetime: tool({
267
270
  description: "Show today's coding time tracked by CodeTime. " +
268
271
  "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() {
272
+ "how long they've been coding, or wants to see their CodeTime stats. " +
273
+ "Supports filtering by project name and showing a breakdown of time across all projects.",
274
+ args: {
275
+ project: tool.schema.string().optional().describe("Filter by project name. Use 'current' to auto-detect the current project. " +
276
+ "Omit to show total time across all projects."),
277
+ breakdown: tool.schema.boolean().optional().describe("When true, show a breakdown of coding time across all projects today."),
278
+ },
279
+ async execute(args) {
272
280
  if (!_token) {
273
281
  return "CodeTime is not configured. Set CODETIME_TOKEN environment variable to enable tracking. Get your token from https://codetime.dev/dashboard/settings";
274
282
  }
275
283
  try {
284
+ // Breakdown mode: show all projects ranked by time
285
+ if (args.breakdown) {
286
+ const projects = await getTopProjects(_token);
287
+ if (projects === null || projects.length === 0) {
288
+ return "No project data available for today.";
289
+ }
290
+ // Calculate total
291
+ const totalMinutes = projects.reduce((sum, p) => sum + p.minutes, 0);
292
+ // Find the longest project name for alignment
293
+ const maxNameLen = Math.max(...projects.map((p) => p.field.length), "Total".length);
294
+ const lines = ["Today's coding time by project:", ""];
295
+ for (const p of projects) {
296
+ const name = p.field.padEnd(maxNameLen + 2);
297
+ lines.push(` ${name}${formatMinutes(p.minutes)}`);
298
+ }
299
+ lines.push(` ${"─".repeat(maxNameLen + 2 + 8)}`);
300
+ lines.push(` ${"Total".padEnd(maxNameLen + 2)}${formatMinutes(totalMinutes)}`);
301
+ return lines.join("\n");
302
+ }
303
+ // Project-specific mode
304
+ const projectName = args.project === "current" ? _projectName : args.project;
305
+ if (projectName) {
306
+ // Fetch both project-specific and total in parallel
307
+ const [projectMins, totalMins] = await Promise.all([
308
+ getProjectMinutes(_token, projectName),
309
+ getTodayMinutes(_token),
310
+ ]);
311
+ if (projectMins === null) {
312
+ return `Failed to fetch coding time for project "${projectName}" from CodeTime API.`;
313
+ }
314
+ const projectFormatted = formatMinutes(projectMins);
315
+ const displayName = args.project === "current"
316
+ ? _projectName
317
+ : projectName;
318
+ if (totalMins !== null) {
319
+ const totalFormatted = formatMinutes(totalMins);
320
+ return `Today's coding time for ${displayName}: ${projectFormatted} (Total across all projects: ${totalFormatted})`;
321
+ }
322
+ return `Today's coding time for ${displayName}: ${projectFormatted}`;
323
+ }
324
+ // Default: total coding time (original behavior)
276
325
  const minutes = await getTodayMinutes(_token);
277
326
  if (minutes === null) {
278
327
  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.1",
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",