@techdivision/opencode-time-tracking 0.2.0 → 0.3.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 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.2.0",
3
+ "version": "0.3.1",
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
 
@@ -105,11 +105,17 @@ export function createEventHook(
105
105
  return
106
106
  }
107
107
 
108
- // Track token usage from step-finish events
108
+ // Track token usage and agent from message part events
109
109
  if (event.type === "message.part.updated") {
110
110
  const props = event.properties as MessagePartUpdatedProperties
111
111
  const part = props.part
112
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
113
119
  if (part.type === "step-finish" && part.sessionID && part.tokens) {
114
120
  sessionManager.addTokenUsage(part.sessionID, {
115
121
  input: part.tokens.input,
@@ -161,6 +167,9 @@ export function createEventHook(
161
167
  ? `${session.model.providerID}/${session.model.modelID}`
162
168
  : null
163
169
 
170
+ // Get agent name if available
171
+ const agentString = session.agent?.name ?? null
172
+
164
173
  try {
165
174
  await csvWriter.write({
166
175
  ticket: session.ticket,
@@ -171,6 +180,7 @@ export function createEventHook(
171
180
  notes: `Auto-tracked: ${toolSummary}`,
172
181
  tokenUsage: session.tokenUsage,
173
182
  model: modelString,
183
+ agent: agentString,
174
184
  })
175
185
 
176
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,58 @@ 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
+
19
+ /**
20
+ * Loads a value from a .env file in the specified directory.
21
+ *
22
+ * @param directory - The directory containing the .env file
23
+ * @param key - The environment variable key to look for
24
+ * @returns The value if found, or `null` if not found
25
+ *
26
+ * @internal
27
+ */
28
+ async function loadEnvValue(
29
+ directory: string,
30
+ key: string
31
+ ): Promise<string | null> {
32
+ const envPath = `${directory}/.env`
33
+
34
+ try {
35
+ const file = Bun.file(envPath)
36
+
37
+ if (await file.exists()) {
38
+ const content = await file.text()
39
+ // Match KEY=value, handling optional quotes
40
+ const regex = new RegExp(`^${key}=(.*)$`, "m")
41
+ const match = content.match(regex)
42
+
43
+ if (match) {
44
+ // Remove surrounding quotes (single or double) and trim
45
+ return match[1].trim().replace(/^["']|["']$/g, "")
46
+ }
47
+ }
48
+
49
+ return null
50
+ } catch {
51
+ return null
52
+ }
53
+ }
54
+
12
55
  /**
13
56
  * Loads the plugin configuration from the project directory.
14
57
  *
15
58
  * @remarks
16
59
  * The configuration file is expected at `.opencode/opencode-project.json`
17
60
  * within the project directory, with a `time_tracking` section.
61
+ *
62
+ * The `user_email` is resolved from (in order of priority):
63
+ * 1. `OPENCODE_USER_EMAIL` system environment variable
64
+ * 2. `OPENCODE_USER_EMAIL` from project `.env` file
65
+ * 3. System username (fallback)
18
66
  */
19
67
  export class ConfigLoader {
20
68
  /**
@@ -28,6 +76,7 @@ export class ConfigLoader {
28
76
  * const config = await ConfigLoader.load("/path/to/project")
29
77
  * if (config) {
30
78
  * console.log(config.csv_file)
79
+ * console.log(config.user_email) // Resolved from ENV, .env file, or system username
31
80
  * }
32
81
  * ```
33
82
  */
@@ -41,7 +90,20 @@ export class ConfigLoader {
41
90
  const projectConfig = (await file.json()) as OpencodeProjectConfig
42
91
 
43
92
  if (projectConfig.time_tracking) {
44
- return projectConfig.time_tracking
93
+ const jsonConfig = projectConfig.time_tracking
94
+
95
+ // Resolve user_email with fallback chain:
96
+ // 1. System environment variable
97
+ // 2. Project .env file
98
+ // 3. System username
99
+ const envValue = await loadEnvValue(directory, ENV_USER_EMAIL)
100
+ const userEmail =
101
+ process.env[ENV_USER_EMAIL] || envValue || userInfo().username
102
+
103
+ return {
104
+ ...jsonConfig,
105
+ user_email: userEmail,
106
+ }
45
107
  }
46
108
  }
47
109
 
@@ -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,model"
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.
@@ -123,6 +123,7 @@ export class CsvWriter {
123
123
  CsvFormatter.escape(data.description),
124
124
  CsvFormatter.escape(data.notes),
125
125
  data.model ?? "",
126
+ data.agent ?? "",
126
127
  ]
127
128
 
128
129
  const csvLine = fields.map((f) => `"${f}"`).join(",")
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import type { ActivityData } from "../types/ActivityData"
6
+ import type { AgentInfo } from "../types/AgentInfo"
6
7
  import type { ModelInfo } from "../types/ModelInfo"
7
8
  import type { SessionData } from "../types/SessionData"
8
9
  import type { TokenUsage } from "../types/TokenUsage"
@@ -63,6 +64,7 @@ export class SessionManager {
63
64
  cacheWrite: 0,
64
65
  },
65
66
  model: null,
67
+ agent: null,
66
68
  }
67
69
 
68
70
  this.sessions.set(sessionID, session)
@@ -146,4 +148,26 @@ export class SessionManager {
146
148
  session.model = model
147
149
  }
148
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
+ }
149
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
+ }
@@ -36,4 +36,13 @@ export interface CsvEntryData {
36
36
  * Examples: `anthropic/claude-opus-4`, `openai/gpt-5`
37
37
  */
38
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
39
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
  }
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import type { ActivityData } from "./ActivityData"
6
+ import type { AgentInfo } from "./AgentInfo"
6
7
  import type { ModelInfo } from "./ModelInfo"
7
8
  import type { TokenUsage } from "./TokenUsage"
8
9
 
@@ -24,4 +25,7 @@ export interface SessionData {
24
25
 
25
26
  /** Model used in this session, or `null` if not detected */
26
27
  model: ModelInfo | null
28
+
29
+ /** First agent used in this session, or `null` if not detected */
30
+ agent: AgentInfo | null
27
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
  }