@techdivision/opencode-time-tracking 0.1.9 → 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.
- package/README.md +20 -1
- package/package.json +1 -1
- package/src/Plugin.ts +12 -6
- package/src/hooks/EventHook.ts +48 -5
- package/src/services/ConfigLoader.ts +21 -1
- package/src/services/CsvWriter.ts +3 -1
- package/src/services/SessionManager.ts +44 -0
- package/src/types/AgentInfo.ts +14 -0
- package/src/types/CsvEntryData.ts +17 -0
- package/src/types/MessagePartUpdatedProperties.ts +5 -2
- package/src/types/ModelInfo.ts +28 -0
- package/src/types/SessionData.ts +8 -0
- package/src/types/TimeTrackingConfig.ts +26 -6
package/README.md
CHANGED
|
@@ -14,6 +14,8 @@ Add to your `opencode.json`:
|
|
|
14
14
|
|
|
15
15
|
## Configuration
|
|
16
16
|
|
|
17
|
+
### 1. Project Configuration
|
|
18
|
+
|
|
17
19
|
Add the `time_tracking` section to your `.opencode/opencode-project.json`:
|
|
18
20
|
|
|
19
21
|
```json
|
|
@@ -21,12 +23,29 @@ Add the `time_tracking` section to your `.opencode/opencode-project.json`:
|
|
|
21
23
|
"$schema": "https://raw.githubusercontent.com/techdivision/opencode-plugins/main/schemas/opencode-project.json",
|
|
22
24
|
"time_tracking": {
|
|
23
25
|
"csv_file": "~/time_tracking/time-tracking.csv",
|
|
24
|
-
"user_email": "your@email.com",
|
|
25
26
|
"default_account_key": "YOUR_ACCOUNT_KEY"
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
```
|
|
29
30
|
|
|
31
|
+
### 2. User Email (Environment Variable)
|
|
32
|
+
|
|
33
|
+
Set your user email via the `OPENCODE_USER_EMAIL` environment variable.
|
|
34
|
+
|
|
35
|
+
Add to your `.env` file (recommended):
|
|
36
|
+
|
|
37
|
+
```env
|
|
38
|
+
OPENCODE_USER_EMAIL=your@email.com
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or export in your shell:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
export OPENCODE_USER_EMAIL=your@email.com
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If not set, the system username is used as fallback.
|
|
48
|
+
|
|
30
49
|
## How it works
|
|
31
50
|
|
|
32
51
|
- Tracks tool executions during each session turn
|
package/package.json
CHANGED
package/src/Plugin.ts
CHANGED
|
@@ -25,20 +25,27 @@ import { createToolExecuteAfterHook } from "./hooks/ToolExecuteAfterHook"
|
|
|
25
25
|
* - Token consumption (input/output/reasoning tokens)
|
|
26
26
|
* - Ticket references (extracted from user messages or todos)
|
|
27
27
|
*
|
|
28
|
-
* Data is exported to a CSV file configured in `.opencode/
|
|
28
|
+
* Data is exported to a CSV file configured in `.opencode/opencode-project.json`.
|
|
29
29
|
*
|
|
30
30
|
* @param input - Plugin input containing client, directory, and other context
|
|
31
31
|
* @returns Hooks object with event and tool.execute.after handlers
|
|
32
32
|
*
|
|
33
33
|
* @example
|
|
34
34
|
* ```json
|
|
35
|
-
* // .opencode/
|
|
35
|
+
* // .opencode/opencode-project.json
|
|
36
36
|
* {
|
|
37
|
-
* "
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* "time_tracking": {
|
|
38
|
+
* "csv_file": "~/worklogs/time.csv",
|
|
39
|
+
* "default_account_key": "ACCOUNT-1"
|
|
40
|
+
* }
|
|
40
41
|
* }
|
|
41
42
|
* ```
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```bash
|
|
46
|
+
* # .env - Set user email via environment variable
|
|
47
|
+
* OPENCODE_USER_EMAIL=user@example.com
|
|
48
|
+
* ```
|
|
42
49
|
*/
|
|
43
50
|
export const plugin: Plugin = async ({
|
|
44
51
|
client,
|
|
@@ -48,7 +55,6 @@ export const plugin: Plugin = async ({
|
|
|
48
55
|
|
|
49
56
|
if (!config) {
|
|
50
57
|
// Silently return empty hooks if no config found
|
|
51
|
-
// Toast notifications don't work during plugin initialization
|
|
52
58
|
return {}
|
|
53
59
|
}
|
|
54
60
|
|
package/src/hooks/EventHook.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @fileoverview Event hook for session lifecycle and token tracking.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { Event } from "@opencode-ai/sdk"
|
|
5
|
+
import type { AssistantMessage, Event, Message } from "@opencode-ai/sdk"
|
|
6
6
|
|
|
7
7
|
import type { CsvWriter } from "../services/CsvWriter"
|
|
8
8
|
import type { SessionManager } from "../services/SessionManager"
|
|
@@ -12,6 +12,13 @@ import type { OpencodeClient } from "../types/OpencodeClient"
|
|
|
12
12
|
|
|
13
13
|
import { DescriptionGenerator } from "../utils/DescriptionGenerator"
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Properties for message.updated events.
|
|
17
|
+
*/
|
|
18
|
+
interface MessageUpdatedProperties {
|
|
19
|
+
info: Message
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
/**
|
|
16
23
|
* Extracts the summary title from the last user message.
|
|
17
24
|
*
|
|
@@ -60,10 +67,11 @@ async function extractSummaryTitle(
|
|
|
60
67
|
* @returns The event hook function
|
|
61
68
|
*
|
|
62
69
|
* @remarks
|
|
63
|
-
* Handles
|
|
70
|
+
* Handles three types of events:
|
|
64
71
|
*
|
|
65
|
-
* 1. **message.
|
|
66
|
-
* 2. **
|
|
72
|
+
* 1. **message.updated** - Tracks model from assistant messages
|
|
73
|
+
* 2. **message.part.updated** - Tracks token usage from step-finish parts
|
|
74
|
+
* 3. **session.idle** - Finalizes and exports the session
|
|
67
75
|
*
|
|
68
76
|
* @example
|
|
69
77
|
* ```typescript
|
|
@@ -78,11 +86,36 @@ export function createEventHook(
|
|
|
78
86
|
client: OpencodeClient
|
|
79
87
|
) {
|
|
80
88
|
return async ({ event }: { event: Event }): Promise<void> => {
|
|
81
|
-
// Track
|
|
89
|
+
// Track model from assistant messages
|
|
90
|
+
if (event.type === "message.updated") {
|
|
91
|
+
const props = event.properties as MessageUpdatedProperties
|
|
92
|
+
const message = props.info
|
|
93
|
+
|
|
94
|
+
if (message.role === "assistant") {
|
|
95
|
+
const assistantMsg = message as AssistantMessage
|
|
96
|
+
|
|
97
|
+
if (assistantMsg.modelID && assistantMsg.providerID) {
|
|
98
|
+
sessionManager.setModel(assistantMsg.sessionID, {
|
|
99
|
+
modelID: assistantMsg.modelID,
|
|
100
|
+
providerID: assistantMsg.providerID,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Track token usage and agent from message part events
|
|
82
109
|
if (event.type === "message.part.updated") {
|
|
83
110
|
const props = event.properties as MessagePartUpdatedProperties
|
|
84
111
|
const part = props.part
|
|
85
112
|
|
|
113
|
+
// Track agent from agent parts (only first agent is stored)
|
|
114
|
+
if (part.type === "agent" && part.sessionID && part.name) {
|
|
115
|
+
sessionManager.setAgent(part.sessionID, part.name)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Track token usage from step-finish events
|
|
86
119
|
if (part.type === "step-finish" && part.sessionID && part.tokens) {
|
|
87
120
|
sessionManager.addTokenUsage(part.sessionID, {
|
|
88
121
|
input: part.tokens.input,
|
|
@@ -129,6 +162,14 @@ export function createEventHook(
|
|
|
129
162
|
session.tokenUsage.output +
|
|
130
163
|
session.tokenUsage.reasoning
|
|
131
164
|
|
|
165
|
+
// Format model as providerID/modelID
|
|
166
|
+
const modelString = session.model
|
|
167
|
+
? `${session.model.providerID}/${session.model.modelID}`
|
|
168
|
+
: null
|
|
169
|
+
|
|
170
|
+
// Get agent name if available
|
|
171
|
+
const agentString = session.agent?.name ?? null
|
|
172
|
+
|
|
132
173
|
try {
|
|
133
174
|
await csvWriter.write({
|
|
134
175
|
ticket: session.ticket,
|
|
@@ -138,6 +179,8 @@ export function createEventHook(
|
|
|
138
179
|
description,
|
|
139
180
|
notes: `Auto-tracked: ${toolSummary}`,
|
|
140
181
|
tokenUsage: session.tokenUsage,
|
|
182
|
+
model: modelString,
|
|
183
|
+
agent: agentString,
|
|
141
184
|
})
|
|
142
185
|
|
|
143
186
|
const minutes = Math.round(durationSeconds / 60)
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* @fileoverview Configuration loader for the time tracking plugin.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { userInfo } from "os"
|
|
6
|
+
|
|
5
7
|
import type {
|
|
6
8
|
OpencodeProjectConfig,
|
|
7
9
|
TimeTrackingConfig,
|
|
@@ -9,12 +11,21 @@ import type {
|
|
|
9
11
|
|
|
10
12
|
import "../types/Bun"
|
|
11
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Environment variable name for user email.
|
|
16
|
+
*/
|
|
17
|
+
const ENV_USER_EMAIL = "OPENCODE_USER_EMAIL"
|
|
18
|
+
|
|
12
19
|
/**
|
|
13
20
|
* Loads the plugin configuration from the project directory.
|
|
14
21
|
*
|
|
15
22
|
* @remarks
|
|
16
23
|
* The configuration file is expected at `.opencode/opencode-project.json`
|
|
17
24
|
* within the project directory, with a `time_tracking` section.
|
|
25
|
+
*
|
|
26
|
+
* The `user_email` is resolved from:
|
|
27
|
+
* 1. `OPENCODE_USER_EMAIL` environment variable
|
|
28
|
+
* 2. System username (fallback)
|
|
18
29
|
*/
|
|
19
30
|
export class ConfigLoader {
|
|
20
31
|
/**
|
|
@@ -28,6 +39,7 @@ export class ConfigLoader {
|
|
|
28
39
|
* const config = await ConfigLoader.load("/path/to/project")
|
|
29
40
|
* if (config) {
|
|
30
41
|
* console.log(config.csv_file)
|
|
42
|
+
* console.log(config.user_email) // Resolved from ENV or system username
|
|
31
43
|
* }
|
|
32
44
|
* ```
|
|
33
45
|
*/
|
|
@@ -41,7 +53,15 @@ export class ConfigLoader {
|
|
|
41
53
|
const projectConfig = (await file.json()) as OpencodeProjectConfig
|
|
42
54
|
|
|
43
55
|
if (projectConfig.time_tracking) {
|
|
44
|
-
|
|
56
|
+
const jsonConfig = projectConfig.time_tracking
|
|
57
|
+
|
|
58
|
+
// Resolve user_email from environment variable or fallback to system username
|
|
59
|
+
const userEmail = process.env[ENV_USER_EMAIL] || userInfo().username
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
...jsonConfig,
|
|
63
|
+
user_email: userEmail,
|
|
64
|
+
}
|
|
45
65
|
}
|
|
46
66
|
}
|
|
47
67
|
|
|
@@ -18,7 +18,7 @@ import "../types/Bun"
|
|
|
18
18
|
* Compatible with Jira/Tempo time tracking import.
|
|
19
19
|
*/
|
|
20
20
|
const CSV_HEADER =
|
|
21
|
-
"id,start_date,end_date,user,ticket_name,issue_key,account_key,start_time,end_time,duration_seconds,tokens_used,tokens_remaining,story_points,description,notes"
|
|
21
|
+
"id,start_date,end_date,user,ticket_name,issue_key,account_key,start_time,end_time,duration_seconds,tokens_used,tokens_remaining,story_points,description,notes,model,agent"
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Writes time tracking entries to a CSV file.
|
|
@@ -122,6 +122,8 @@ export class CsvWriter {
|
|
|
122
122
|
"",
|
|
123
123
|
CsvFormatter.escape(data.description),
|
|
124
124
|
CsvFormatter.escape(data.notes),
|
|
125
|
+
data.model ?? "",
|
|
126
|
+
data.agent ?? "",
|
|
125
127
|
]
|
|
126
128
|
|
|
127
129
|
const csvLine = fields.map((f) => `"${f}"`).join(",")
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { ActivityData } from "../types/ActivityData"
|
|
6
|
+
import type { AgentInfo } from "../types/AgentInfo"
|
|
7
|
+
import type { ModelInfo } from "../types/ModelInfo"
|
|
6
8
|
import type { SessionData } from "../types/SessionData"
|
|
7
9
|
import type { TokenUsage } from "../types/TokenUsage"
|
|
8
10
|
|
|
@@ -61,6 +63,8 @@ export class SessionManager {
|
|
|
61
63
|
cacheRead: 0,
|
|
62
64
|
cacheWrite: 0,
|
|
63
65
|
},
|
|
66
|
+
model: null,
|
|
67
|
+
agent: null,
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
this.sessions.set(sessionID, session)
|
|
@@ -126,4 +130,44 @@ export class SessionManager {
|
|
|
126
130
|
session.ticket = ticket
|
|
127
131
|
}
|
|
128
132
|
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Sets the model for a session.
|
|
136
|
+
*
|
|
137
|
+
* @param sessionID - The OpenCode session identifier
|
|
138
|
+
* @param model - The model information
|
|
139
|
+
*
|
|
140
|
+
* @remarks
|
|
141
|
+
* Only sets the model if it hasn't been set yet.
|
|
142
|
+
* The first model detected in a session is used.
|
|
143
|
+
*/
|
|
144
|
+
setModel(sessionID: string, model: ModelInfo): void {
|
|
145
|
+
const session = this.sessions.get(sessionID)
|
|
146
|
+
|
|
147
|
+
if (session && !session.model) {
|
|
148
|
+
session.model = model
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Sets the agent for a session.
|
|
154
|
+
*
|
|
155
|
+
* @param sessionID - The OpenCode session identifier
|
|
156
|
+
* @param agentName - The agent name (e.g., "@developer")
|
|
157
|
+
*
|
|
158
|
+
* @remarks
|
|
159
|
+
* Only sets the agent if it hasn't been set yet.
|
|
160
|
+
* The first agent detected in a session is used (primary agent).
|
|
161
|
+
*/
|
|
162
|
+
setAgent(sessionID: string, agentName: string): void {
|
|
163
|
+
const session = this.sessions.get(sessionID)
|
|
164
|
+
|
|
165
|
+
if (session && !session.agent) {
|
|
166
|
+
const agent: AgentInfo = {
|
|
167
|
+
name: agentName,
|
|
168
|
+
timestamp: Date.now(),
|
|
169
|
+
}
|
|
170
|
+
session.agent = agent
|
|
171
|
+
}
|
|
172
|
+
}
|
|
129
173
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Agent information type for tracking which agent executed work.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Information about an agent that was active during a session.
|
|
7
|
+
*/
|
|
8
|
+
export interface AgentInfo {
|
|
9
|
+
/** The agent name (e.g., "@developer", "@reviewer") */
|
|
10
|
+
name: string
|
|
11
|
+
|
|
12
|
+
/** Unix timestamp in milliseconds when the agent became active */
|
|
13
|
+
timestamp: number
|
|
14
|
+
}
|
|
@@ -28,4 +28,21 @@ export interface CsvEntryData {
|
|
|
28
28
|
|
|
29
29
|
/** Token consumption statistics */
|
|
30
30
|
tokenUsage: TokenUsage
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Model identifier in format `providerID/modelID`.
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* Examples: `anthropic/claude-opus-4`, `openai/gpt-5`
|
|
37
|
+
*/
|
|
38
|
+
model: string | null
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Agent name that performed the work.
|
|
42
|
+
*
|
|
43
|
+
* @remarks
|
|
44
|
+
* Examples: `@developer`, `@reviewer`
|
|
45
|
+
* Only the first/primary agent is tracked.
|
|
46
|
+
*/
|
|
47
|
+
agent: string | null
|
|
31
48
|
}
|
|
@@ -10,13 +10,16 @@ import type { StepFinishPart } from "./StepFinishPart"
|
|
|
10
10
|
export interface MessagePartUpdatedProperties {
|
|
11
11
|
/** The updated message part */
|
|
12
12
|
part: {
|
|
13
|
-
/** The type of the part */
|
|
13
|
+
/** The type of the part (e.g., "step-finish", "agent") */
|
|
14
14
|
type: string
|
|
15
15
|
|
|
16
|
-
/** Session ID (present on step-finish parts) */
|
|
16
|
+
/** Session ID (present on step-finish and agent parts) */
|
|
17
17
|
sessionID?: string
|
|
18
18
|
|
|
19
19
|
/** Token usage (present on step-finish parts) */
|
|
20
20
|
tokens?: StepFinishPart["tokens"]
|
|
21
|
+
|
|
22
|
+
/** Agent name (present on agent parts) */
|
|
23
|
+
name?: string
|
|
21
24
|
}
|
|
22
25
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Model information type for tracking which LLM was used.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Information about the model used in a session.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* Extracted from AssistantMessage events in the OpenCode SDK.
|
|
10
|
+
* Used to calculate token costs per model.
|
|
11
|
+
*/
|
|
12
|
+
export interface ModelInfo {
|
|
13
|
+
/**
|
|
14
|
+
* Model identifier (e.g., "claude-opus-4", "gpt-5").
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* This is the model name as reported by the provider.
|
|
18
|
+
*/
|
|
19
|
+
modelID: string
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Provider identifier (e.g., "anthropic", "openai").
|
|
23
|
+
*
|
|
24
|
+
* @remarks
|
|
25
|
+
* Combined with modelID to form the full model reference: `providerID/modelID`
|
|
26
|
+
*/
|
|
27
|
+
providerID: string
|
|
28
|
+
}
|
package/src/types/SessionData.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { ActivityData } from "./ActivityData"
|
|
6
|
+
import type { AgentInfo } from "./AgentInfo"
|
|
7
|
+
import type { ModelInfo } from "./ModelInfo"
|
|
6
8
|
import type { TokenUsage } from "./TokenUsage"
|
|
7
9
|
|
|
8
10
|
/**
|
|
@@ -20,4 +22,10 @@ export interface SessionData {
|
|
|
20
22
|
|
|
21
23
|
/** Cumulative token usage for the session */
|
|
22
24
|
tokenUsage: TokenUsage
|
|
25
|
+
|
|
26
|
+
/** Model used in this session, or `null` if not detected */
|
|
27
|
+
model: ModelInfo | null
|
|
28
|
+
|
|
29
|
+
/** First agent used in this session, or `null` if not detected */
|
|
30
|
+
agent: AgentInfo | null
|
|
23
31
|
}
|
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Time tracking configuration
|
|
6
|
+
* Time tracking configuration as stored in `.opencode/opencode-project.json`.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* The `user_email` field is not stored in the JSON file.
|
|
10
|
+
* It is resolved from `OPENCODE_USER_EMAIL` environment variable
|
|
11
|
+
* or falls back to the system username.
|
|
7
12
|
*/
|
|
8
|
-
export interface
|
|
13
|
+
export interface TimeTrackingJsonConfig {
|
|
9
14
|
/**
|
|
10
15
|
* Path to the CSV output file.
|
|
11
16
|
*
|
|
@@ -17,13 +22,28 @@ export interface TimeTrackingConfig {
|
|
|
17
22
|
*/
|
|
18
23
|
csv_file: string
|
|
19
24
|
|
|
20
|
-
/** Email address of the user for the worklog */
|
|
21
|
-
user_email: string
|
|
22
|
-
|
|
23
25
|
/** Default Jira account key for time entries */
|
|
24
26
|
default_account_key: string
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Resolved time tracking configuration used at runtime.
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* Extends `TimeTrackingJsonConfig` with the resolved `user_email` field.
|
|
34
|
+
*/
|
|
35
|
+
export interface TimeTrackingConfig extends TimeTrackingJsonConfig {
|
|
36
|
+
/**
|
|
37
|
+
* User email for the worklog.
|
|
38
|
+
*
|
|
39
|
+
* @remarks
|
|
40
|
+
* Resolved from (in order of priority):
|
|
41
|
+
* 1. `OPENCODE_USER_EMAIL` environment variable
|
|
42
|
+
* 2. System username (via `os.userInfo().username`)
|
|
43
|
+
*/
|
|
44
|
+
user_email: string
|
|
45
|
+
}
|
|
46
|
+
|
|
27
47
|
/**
|
|
28
48
|
* OpenCode project configuration structure.
|
|
29
49
|
*/
|
|
@@ -32,5 +52,5 @@ export interface OpencodeProjectConfig {
|
|
|
32
52
|
$schema?: string
|
|
33
53
|
|
|
34
54
|
/** Time tracking configuration */
|
|
35
|
-
time_tracking?:
|
|
55
|
+
time_tracking?: TimeTrackingJsonConfig
|
|
36
56
|
}
|