opencode-codetime 0.3.0 → 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/git.d.ts +4 -9
- package/dist/git.js +26 -16
- package/dist/index.js +68 -50
- 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/git.d.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
interface ShellFn {
|
|
2
|
-
(strings: TemplateStringsArray, ...values: unknown[]): Promise<{
|
|
3
|
-
stdout: Buffer;
|
|
4
|
-
exitCode: number;
|
|
5
|
-
}>;
|
|
6
|
-
}
|
|
7
1
|
/**
|
|
8
2
|
* Extract git remote origin URL from the worktree.
|
|
3
|
+
* Uses node:child_process directly to avoid OpenCode TUI shell flash.
|
|
9
4
|
*/
|
|
10
|
-
export declare function getGitOrigin(
|
|
5
|
+
export declare function getGitOrigin(worktree: string): Promise<string | null>;
|
|
11
6
|
/**
|
|
12
7
|
* Extract the current git branch from the worktree.
|
|
8
|
+
* Uses node:child_process directly to avoid OpenCode TUI shell flash.
|
|
13
9
|
*/
|
|
14
|
-
export declare function getGitBranch(
|
|
15
|
-
export {};
|
|
10
|
+
export declare function getGitBranch(worktree: string): Promise<string | null>;
|
package/dist/git.js
CHANGED
|
@@ -1,32 +1,42 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { execFile as execFileCb } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execFile = promisify(execFileCb);
|
|
2
4
|
/**
|
|
3
5
|
* Extract git remote origin URL from the worktree.
|
|
6
|
+
* Uses node:child_process directly to avoid OpenCode TUI shell flash.
|
|
4
7
|
*/
|
|
5
|
-
export async function getGitOrigin(
|
|
8
|
+
export async function getGitOrigin(worktree) {
|
|
6
9
|
try {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
const { stdout } = await execFile("git", [
|
|
11
|
+
"-C",
|
|
12
|
+
worktree,
|
|
13
|
+
"config",
|
|
14
|
+
"--get",
|
|
15
|
+
"remote.origin.url",
|
|
16
|
+
]);
|
|
17
|
+
return stdout.trim() || null;
|
|
11
18
|
}
|
|
12
19
|
catch {
|
|
13
|
-
|
|
20
|
+
return null;
|
|
14
21
|
}
|
|
15
|
-
return null;
|
|
16
22
|
}
|
|
17
23
|
/**
|
|
18
24
|
* Extract the current git branch from the worktree.
|
|
25
|
+
* Uses node:child_process directly to avoid OpenCode TUI shell flash.
|
|
19
26
|
*/
|
|
20
|
-
export async function getGitBranch(
|
|
27
|
+
export async function getGitBranch(worktree) {
|
|
21
28
|
try {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
const { stdout } = await execFile("git", [
|
|
30
|
+
"-C",
|
|
31
|
+
worktree,
|
|
32
|
+
"rev-parse",
|
|
33
|
+
"--abbrev-ref",
|
|
34
|
+
"HEAD",
|
|
35
|
+
]);
|
|
36
|
+
const branch = stdout.trim();
|
|
37
|
+
return branch && branch !== "HEAD" ? branch : null;
|
|
27
38
|
}
|
|
28
39
|
catch {
|
|
29
|
-
|
|
40
|
+
return null;
|
|
30
41
|
}
|
|
31
|
-
return null;
|
|
32
42
|
}
|
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";
|
|
@@ -10,11 +10,9 @@ import { initState, shouldSendHeartbeat, updateLastHeartbeat, } from "./state.js
|
|
|
10
10
|
const processedCallIds = new Set();
|
|
11
11
|
const pendingFiles = new Map();
|
|
12
12
|
let _token = null;
|
|
13
|
-
let _client = null;
|
|
14
13
|
let _projectName = "unknown";
|
|
15
14
|
let _projectDir = "";
|
|
16
15
|
let _worktree = "";
|
|
17
|
-
let _shellFn = null;
|
|
18
16
|
let _gitOrigin = null;
|
|
19
17
|
let _gitBranch = null;
|
|
20
18
|
let _gitInfoFetched = false;
|
|
@@ -86,12 +84,12 @@ function computeRelativeFile(absoluteFile, projectDir) {
|
|
|
86
84
|
}
|
|
87
85
|
// ---- Lazy git info ----
|
|
88
86
|
async function ensureGitInfo() {
|
|
89
|
-
if (_gitInfoFetched || !
|
|
87
|
+
if (_gitInfoFetched || !_worktree)
|
|
90
88
|
return;
|
|
91
89
|
_gitInfoFetched = true;
|
|
92
90
|
try {
|
|
93
|
-
_gitOrigin = await getGitOrigin(
|
|
94
|
-
_gitBranch = await getGitBranch(
|
|
91
|
+
_gitOrigin = await getGitOrigin(_worktree);
|
|
92
|
+
_gitBranch = await getGitBranch(_worktree);
|
|
95
93
|
await debug("Git info", { origin: _gitOrigin, branch: _gitBranch }).catch(() => { });
|
|
96
94
|
}
|
|
97
95
|
catch {
|
|
@@ -168,7 +166,7 @@ function formatMinutes(minutes) {
|
|
|
168
166
|
// ---- Plugin entry point ----
|
|
169
167
|
export const plugin = async (ctx) => {
|
|
170
168
|
try {
|
|
171
|
-
const { client, directory, worktree
|
|
169
|
+
const { client, directory, worktree } = ctx;
|
|
172
170
|
// Initialize logger (may fail if client shape differs)
|
|
173
171
|
try {
|
|
174
172
|
initLogger(client);
|
|
@@ -195,13 +193,10 @@ export const plugin = async (ctx) => {
|
|
|
195
193
|
}).catch(() => { });
|
|
196
194
|
// Initialize state
|
|
197
195
|
initState();
|
|
198
|
-
_client = client;
|
|
199
196
|
_projectDir = directory;
|
|
200
197
|
_worktree = worktree;
|
|
201
198
|
_projectName = `${path.basename(directory)} [opencode]`;
|
|
202
199
|
_platform = os.platform();
|
|
203
|
-
// Store shell function for lazy git info fetching
|
|
204
|
-
_shellFn = $;
|
|
205
200
|
return {
|
|
206
201
|
event: async ({ event }) => {
|
|
207
202
|
try {
|
|
@@ -258,55 +253,78 @@ export const plugin = async (ctx) => {
|
|
|
258
253
|
await error("Chat message handler error", { error: String(err) }).catch(() => { });
|
|
259
254
|
}
|
|
260
255
|
},
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const minutes = await getTodayMinutes(_token);
|
|
276
|
-
if (minutes === null) {
|
|
277
|
-
await _client.tui.showToast({
|
|
278
|
-
body: { message: "CodeTime: failed to fetch data", variant: "error" },
|
|
279
|
-
}).catch(() => { });
|
|
280
|
-
output.parts.push({
|
|
281
|
-
type: "text",
|
|
282
|
-
text: "Failed to fetch coding time from CodeTime API.",
|
|
283
|
-
});
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
const formatted = formatMinutes(minutes);
|
|
287
|
-
await _client.tui.showToast({
|
|
288
|
-
body: { message: `CodeTime: ${formatted} today`, variant: "success" },
|
|
289
|
-
}).catch(() => { });
|
|
290
|
-
output.parts.push({
|
|
291
|
-
type: "text",
|
|
292
|
-
text: `Today's coding time: ${formatted}`,
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
catch (err) {
|
|
296
|
-
await error("Command handler error", { error: String(err) }).catch(() => { });
|
|
297
|
-
}
|
|
256
|
+
config: async (cfg) => {
|
|
257
|
+
cfg.command = cfg.command || {};
|
|
258
|
+
cfg.command["codetime"] = {
|
|
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.",
|
|
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
|
+
};
|
|
298
270
|
},
|
|
299
271
|
tool: {
|
|
300
272
|
codetime: tool({
|
|
301
273
|
description: "Show today's coding time tracked by CodeTime. " +
|
|
302
274
|
"Use this when the user asks about their coding time, " +
|
|
303
|
-
"how long they've been coding, or wants to see their CodeTime stats."
|
|
304
|
-
|
|
305
|
-
|
|
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) {
|
|
306
283
|
if (!_token) {
|
|
307
284
|
return "CodeTime is not configured. Set CODETIME_TOKEN environment variable to enable tracking. Get your token from https://codetime.dev/dashboard/settings";
|
|
308
285
|
}
|
|
309
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)
|
|
310
328
|
const minutes = await getTodayMinutes(_token);
|
|
311
329
|
if (minutes === null) {
|
|
312
330
|
return "Failed to fetch coding time from CodeTime API.";
|