opencode-codetime 0.2.0 → 0.4.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 +10 -1
- package/dist/git.d.ts +4 -9
- package/dist/git.js +26 -16
- package/dist/index.js +27 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
# opencode-codetime
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/opencode-codetime)
|
|
4
|
+
[](https://www.npmjs.com/package/opencode-codetime)
|
|
5
|
+
[](https://github.com/roman-pinchuk/opencode-codetime/blob/main/LICENSE)
|
|
6
|
+
[](https://github.com/roman-pinchuk/opencode-codetime/releases)
|
|
7
|
+
[](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` |
|
|
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/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
|
@@ -15,6 +15,7 @@ let _projectDir = "";
|
|
|
15
15
|
let _worktree = "";
|
|
16
16
|
let _gitOrigin = null;
|
|
17
17
|
let _gitBranch = null;
|
|
18
|
+
let _gitInfoFetched = false;
|
|
18
19
|
let _platform = os.platform();
|
|
19
20
|
// ---- File extraction from tool events ----
|
|
20
21
|
function extractFilesFromTool(tool, metadata, output, title) {
|
|
@@ -81,6 +82,20 @@ function computeRelativeFile(absoluteFile, projectDir) {
|
|
|
81
82
|
}
|
|
82
83
|
return path.basename(absoluteFile);
|
|
83
84
|
}
|
|
85
|
+
// ---- Lazy git info ----
|
|
86
|
+
async function ensureGitInfo() {
|
|
87
|
+
if (_gitInfoFetched || !_worktree)
|
|
88
|
+
return;
|
|
89
|
+
_gitInfoFetched = true;
|
|
90
|
+
try {
|
|
91
|
+
_gitOrigin = await getGitOrigin(_worktree);
|
|
92
|
+
_gitBranch = await getGitBranch(_worktree);
|
|
93
|
+
await debug("Git info", { origin: _gitOrigin, branch: _gitBranch }).catch(() => { });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Git info is optional, continue without it
|
|
97
|
+
}
|
|
98
|
+
}
|
|
84
99
|
// ---- Heartbeat processing ----
|
|
85
100
|
async function processHeartbeats(force = false) {
|
|
86
101
|
if (!_token)
|
|
@@ -89,6 +104,7 @@ async function processHeartbeats(force = false) {
|
|
|
89
104
|
return;
|
|
90
105
|
if (pendingFiles.size === 0)
|
|
91
106
|
return;
|
|
107
|
+
await ensureGitInfo();
|
|
92
108
|
const promises = [];
|
|
93
109
|
for (const [filePath, fileInfo] of pendingFiles.entries()) {
|
|
94
110
|
const relativeFile = computeRelativeFile(filePath, _projectDir);
|
|
@@ -150,7 +166,7 @@ function formatMinutes(minutes) {
|
|
|
150
166
|
// ---- Plugin entry point ----
|
|
151
167
|
export const plugin = async (ctx) => {
|
|
152
168
|
try {
|
|
153
|
-
const { client, directory, worktree
|
|
169
|
+
const { client, directory, worktree } = ctx;
|
|
154
170
|
// Initialize logger (may fail if client shape differs)
|
|
155
171
|
try {
|
|
156
172
|
initLogger(client);
|
|
@@ -179,15 +195,8 @@ export const plugin = async (ctx) => {
|
|
|
179
195
|
initState();
|
|
180
196
|
_projectDir = directory;
|
|
181
197
|
_worktree = worktree;
|
|
182
|
-
_projectName =
|
|
198
|
+
_projectName = `${path.basename(directory)} [opencode]`;
|
|
183
199
|
_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
|
-
}
|
|
191
200
|
return {
|
|
192
201
|
event: async ({ event }) => {
|
|
193
202
|
try {
|
|
@@ -244,6 +253,15 @@ export const plugin = async (ctx) => {
|
|
|
244
253
|
await error("Chat message handler error", { error: String(err) }).catch(() => { });
|
|
245
254
|
}
|
|
246
255
|
},
|
|
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
|
+
},
|
|
247
265
|
tool: {
|
|
248
266
|
codetime: tool({
|
|
249
267
|
description: "Show today's coding time tracked by CodeTime. " +
|