opencode-codetime 0.2.0 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +10 -1
  2. package/dist/index.js +60 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,12 +1,20 @@
1
1
  # opencode-codetime
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/opencode-codetime)](https://www.npmjs.com/package/opencode-codetime)
4
+ [![npm downloads](https://img.shields.io/npm/dm/opencode-codetime)](https://www.npmjs.com/package/opencode-codetime)
5
+ [![license](https://img.shields.io/npm/l/opencode-codetime)](https://github.com/roman-pinchuk/opencode-codetime/blob/main/LICENSE)
6
+ [![GitHub release](https://img.shields.io/github/v/release/roman-pinchuk/opencode-codetime)](https://github.com/roman-pinchuk/opencode-codetime/releases)
7
+ [![CI](https://img.shields.io/github/actions/workflow/status/roman-pinchuk/opencode-codetime/publish.yml?label=publish)](https://github.com/roman-pinchuk/opencode-codetime/actions)
8
+
3
9
  [CodeTime](https://codetime.dev) plugin for [OpenCode](https://github.com/anomalyco/opencode) -- track your AI coding activity and time spent.
4
10
 
5
11
  ## Features
6
12
 
7
13
  - **Automatic time tracking** -- sends coding events to CodeTime when OpenCode reads, edits, or writes files
14
+ - **Check your coding time** -- ask the AI "what's my coding time?" and it fetches your stats via the `codetime` tool
8
15
  - **Language detection** -- detects 90+ programming languages from file extensions
9
16
  - **Git integration** -- captures current branch and remote origin
17
+ - **Project identification** -- shows as `[opencode] project-name` on your CodeTime dashboard
10
18
  - **Rate-limited** -- one heartbeat per 2 minutes to avoid API spam
11
19
  - **Session lifecycle** -- flushes pending events on session end so no data is lost
12
20
  - **Zero config** -- just set your token and go
@@ -81,7 +89,7 @@ Each heartbeat sent to CodeTime includes:
81
89
  |-------|-------|
82
90
  | `eventTime` | Unix timestamp of the event |
83
91
  | `language` | Detected from file extension (e.g. `TypeScript`, `Python`) |
84
- | `project` | Current directory name |
92
+ | `project` | `[opencode] directory-name` |
85
93
  | `relativeFile` | File path relative to the project root |
86
94
  | `editor` | `opencode` |
87
95
  | `platform` | OS platform (`darwin`, `linux`, `windows`) |
@@ -94,6 +102,7 @@ Each heartbeat sent to CodeTime includes:
94
102
  |------|---------|
95
103
  | `event` | Listens for `message.part.updated` (tool completions) and `session.idle`/`session.deleted` (flush) |
96
104
  | `chat.message` | Processes pending heartbeats on chat activity |
105
+ | `tool` | Registers `codetime` tool to check today's coding time |
97
106
 
98
107
  ### Tool tracking
99
108
 
package/dist/index.js CHANGED
@@ -10,11 +10,14 @@ 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;
13
14
  let _projectName = "unknown";
14
15
  let _projectDir = "";
15
16
  let _worktree = "";
17
+ let _shellFn = null;
16
18
  let _gitOrigin = null;
17
19
  let _gitBranch = null;
20
+ let _gitInfoFetched = false;
18
21
  let _platform = os.platform();
19
22
  // ---- File extraction from tool events ----
20
23
  function extractFilesFromTool(tool, metadata, output, title) {
@@ -81,6 +84,20 @@ function computeRelativeFile(absoluteFile, projectDir) {
81
84
  }
82
85
  return path.basename(absoluteFile);
83
86
  }
87
+ // ---- Lazy git info ----
88
+ async function ensureGitInfo() {
89
+ if (_gitInfoFetched || !_shellFn || !_worktree)
90
+ return;
91
+ _gitInfoFetched = true;
92
+ try {
93
+ _gitOrigin = await getGitOrigin(_shellFn, _worktree);
94
+ _gitBranch = await getGitBranch(_shellFn, _worktree);
95
+ await debug("Git info", { origin: _gitOrigin, branch: _gitBranch }).catch(() => { });
96
+ }
97
+ catch {
98
+ // Git info is optional, continue without it
99
+ }
100
+ }
84
101
  // ---- Heartbeat processing ----
85
102
  async function processHeartbeats(force = false) {
86
103
  if (!_token)
@@ -89,6 +106,7 @@ async function processHeartbeats(force = false) {
89
106
  return;
90
107
  if (pendingFiles.size === 0)
91
108
  return;
109
+ await ensureGitInfo();
92
110
  const promises = [];
93
111
  for (const [filePath, fileInfo] of pendingFiles.entries()) {
94
112
  const relativeFile = computeRelativeFile(filePath, _projectDir);
@@ -177,17 +195,13 @@ export const plugin = async (ctx) => {
177
195
  }).catch(() => { });
178
196
  // Initialize state
179
197
  initState();
198
+ _client = client;
180
199
  _projectDir = directory;
181
200
  _worktree = worktree;
182
- _projectName = `[opencode] ${path.basename(directory)}`;
201
+ _projectName = `${path.basename(directory)} [opencode]`;
183
202
  _platform = os.platform();
184
- // Fetch git info
185
- if (worktree) {
186
- const shellFn = $;
187
- _gitOrigin = await getGitOrigin(shellFn, worktree);
188
- _gitBranch = await getGitBranch(shellFn, worktree);
189
- await debug("Git info", { origin: _gitOrigin, branch: _gitBranch }).catch(() => { });
190
- }
203
+ // Store shell function for lazy git info fetching
204
+ _shellFn = $;
191
205
  return {
192
206
  event: async ({ event }) => {
193
207
  try {
@@ -244,6 +258,44 @@ export const plugin = async (ctx) => {
244
258
  await error("Chat message handler error", { error: String(err) }).catch(() => { });
245
259
  }
246
260
  },
261
+ "command.execute.before": async (input, output) => {
262
+ try {
263
+ if (input.command !== "codetime")
264
+ return;
265
+ if (!_token || !_client) {
266
+ await _client?.tui.showToast({
267
+ body: { message: "CodeTime: token not configured", variant: "error" },
268
+ }).catch(() => { });
269
+ output.parts.push({
270
+ type: "text",
271
+ text: "CodeTime is not configured. Set `CODETIME_TOKEN` environment variable to enable tracking.\nGet your token from https://codetime.dev/dashboard/settings",
272
+ });
273
+ return;
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
+ }
298
+ },
247
299
  tool: {
248
300
  codetime: tool({
249
301
  description: "Show today's coding time tracked by CodeTime. " +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-codetime",
3
- "version": "0.2.0",
3
+ "version": "0.3.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",