@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techdivision/opencode-time-tracking",
3
- "version": "0.1.9",
3
+ "version": "0.3.0",
4
4
  "description": "Automatic time tracking plugin for OpenCode - tracks session duration and tool usage to CSV",
5
5
  "main": "src/Plugin.ts",
6
6
  "types": "src/Plugin.ts",
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/time-tracking.json`.
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/time-tracking.json
35
+ * // .opencode/opencode-project.json
36
36
  * {
37
- * "csv_file": "~/worklogs/time.csv",
38
- * "user_email": "user@example.com",
39
- * "default_account_key": "ACCOUNT-1"
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
 
@@ -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 two types of events:
70
+ * Handles three types of events:
64
71
  *
65
- * 1. **message.part.updated** - Tracks token usage from step-finish parts
66
- * 2. **session.idle** - Finalizes and exports the session
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 token usage from step-finish events
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
- return projectConfig.time_tracking
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
+ }
@@ -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 from `.opencode/opencode-project.json`.
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 TimeTrackingConfig {
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?: TimeTrackingConfig
55
+ time_tracking?: TimeTrackingJsonConfig
36
56
  }