opencode-time-tracking 0.1.5

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/AGENTS.md ADDED
@@ -0,0 +1,247 @@
1
+ # AGENTS.md - OpenCode Time Tracking Plugin
2
+
3
+ Guidelines for AI agents working in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ OpenCode plugin that automatically tracks session duration, tool usage, and token consumption, exporting data to CSV for time tracking integration (e.g., Jira/Tempo).
8
+
9
+ ## Build & Development Commands
10
+
11
+ ```bash
12
+ # Install dependencies
13
+ npm install
14
+
15
+ # Type check (no emit)
16
+ npx tsc --noEmit
17
+
18
+ # Watch mode for development
19
+ npx tsc --noEmit --watch
20
+ ```
21
+
22
+ **Note:** This is a Bun-based plugin. No build step required - TypeScript files are loaded directly by OpenCode at runtime.
23
+
24
+ ## Project Structure
25
+
26
+ ```
27
+ src/
28
+ ├── Plugin.ts # Main entry point, exports plugin
29
+ ├── hooks/ # OpenCode hook implementations
30
+ │ ├── EventHook.ts # Session events (idle, deleted, token tracking)
31
+ │ └── ToolExecuteAfterHook.ts # Tool execution tracking
32
+ ├── services/ # Business logic classes
33
+ │ ├── ConfigLoader.ts # Load plugin configuration
34
+ │ ├── CsvWriter.ts # CSV file output
35
+ │ ├── SessionManager.ts # Session state management
36
+ │ └── TicketExtractor.ts # Extract tickets from messages/todos
37
+ ├── types/ # TypeScript interfaces (one per file)
38
+ │ ├── ActivityData.ts
39
+ │ ├── SessionData.ts
40
+ │ ├── TokenUsage.ts
41
+ │ └── ...
42
+ └── utils/ # Utility classes
43
+ ├── CsvFormatter.ts # CSV formatting helpers
44
+ └── DescriptionGenerator.ts # Generate activity descriptions
45
+ ```
46
+
47
+ ## Code Style Guidelines
48
+
49
+ ### File Organization
50
+
51
+ **CRITICAL: One class/interface/function per file using PascalCase naming.**
52
+
53
+ ```
54
+ # Good
55
+ src/types/TokenUsage.ts → export interface TokenUsage
56
+ src/services/SessionManager.ts → export class SessionManager
57
+ src/hooks/EventHook.ts → export function createEventHook
58
+
59
+ # Bad - multiple exports in one file
60
+ src/types/index.ts → export interface A, B, C // NO!
61
+ ```
62
+
63
+ ### Imports
64
+
65
+ - Use `import type` for type-only imports
66
+ - Group imports: external packages first, then internal modules
67
+ - Use relative paths for internal imports
68
+
69
+ ```typescript
70
+ // External packages first
71
+ import type { Plugin, Hooks, PluginInput } from "@opencode-ai/plugin"
72
+ import type { Event } from "@opencode-ai/sdk"
73
+
74
+ // Internal imports
75
+ import type { SessionManager } from "../services/SessionManager"
76
+ import { DescriptionGenerator } from "../utils/DescriptionGenerator"
77
+ ```
78
+
79
+ ### TypeScript
80
+
81
+ - Strict mode enabled (`"strict": true`)
82
+ - Target: ESNext
83
+ - Module resolution: bundler
84
+ - No emit - TypeScript is for type checking only
85
+ - Explicit return types on public methods
86
+ - Use `interface` for object shapes, `type` for unions/aliases
87
+
88
+ ```typescript
89
+ // Interface for object shapes
90
+ export interface TokenUsage {
91
+ input: number
92
+ output: number
93
+ }
94
+
95
+ // Type for unions or derived types
96
+ type OpencodeClient = ReturnType<typeof createOpencodeClient>
97
+ ```
98
+
99
+ ### Naming Conventions
100
+
101
+ | Type | Convention | Example |
102
+ |------|------------|---------|
103
+ | Files | PascalCase | `SessionManager.ts` |
104
+ | Classes | PascalCase | `class SessionManager` |
105
+ | Interfaces | PascalCase | `interface TokenUsage` |
106
+ | Functions | camelCase | `createEventHook()` |
107
+ | Variables | camelCase | `sessionManager` |
108
+ | Constants | UPPER_SNAKE_CASE | `TICKET_PATTERN` |
109
+ | Private members | camelCase with `private` | `private sessions` |
110
+
111
+ ### Error Handling
112
+
113
+ - Use try/catch with empty catch blocks for graceful degradation
114
+ - Return `null` on failure rather than throwing
115
+ - Log errors via toast notifications to user
116
+
117
+ ```typescript
118
+ try {
119
+ const result = await client.session.messages(...)
120
+ // ...
121
+ } catch {
122
+ return null // Graceful fallback
123
+ }
124
+ ```
125
+
126
+ ### Class Structure
127
+
128
+ ```typescript
129
+ export class ServiceName {
130
+ // Private fields first
131
+ private client: OpencodeClient
132
+
133
+ // Constructor
134
+ constructor(client: OpencodeClient) {
135
+ this.client = client
136
+ }
137
+
138
+ // Public methods
139
+ async publicMethod(): Promise<string | null> {
140
+ // ...
141
+ }
142
+
143
+ // Private methods last
144
+ private helperMethod(): void {
145
+ // ...
146
+ }
147
+ }
148
+ ```
149
+
150
+ ### Hook Factory Pattern
151
+
152
+ Hooks are created via factory functions that receive dependencies:
153
+
154
+ ```typescript
155
+ export function createEventHook(
156
+ sessionManager: SessionManager,
157
+ csvWriter: CsvWriter,
158
+ client: OpencodeClient
159
+ ) {
160
+ return async ({ event }: { event: Event }): Promise<void> => {
161
+ // Hook implementation
162
+ }
163
+ }
164
+ ```
165
+
166
+ ## OpenCode SDK Usage
167
+
168
+ ### Client API Calls
169
+
170
+ Use `path` parameter for session-specific endpoints:
171
+
172
+ ```typescript
173
+ // Correct
174
+ const result = await client.session.messages({
175
+ path: { id: sessionID },
176
+ } as Parameters<typeof client.session.messages>[0])
177
+
178
+ // Access data
179
+ const messages = result.data as MessageWithParts[]
180
+ ```
181
+
182
+ ### Plugin Export
183
+
184
+ Must export a named `plugin` constant:
185
+
186
+ ```typescript
187
+ export const plugin: Plugin = async ({ client, directory }: PluginInput): Promise<Hooks> => {
188
+ return {
189
+ "tool.execute.after": createToolExecuteAfterHook(...),
190
+ event: createEventHook(...),
191
+ }
192
+ }
193
+
194
+ export default plugin
195
+ ```
196
+
197
+ ## Configuration
198
+
199
+ Plugin config file: `.opencode/time-tracking.json`
200
+
201
+ ```json
202
+ {
203
+ "csv_file": "~/worklogs/time.csv",
204
+ "user_email": "user@example.com",
205
+ "default_account_key": "ACCOUNT-1"
206
+ }
207
+ ```
208
+
209
+ ## Git Workflow (Gitflow)
210
+
211
+ This project follows **Gitflow**:
212
+
213
+ - **main**: Production-ready releases only
214
+ - **develop**: Integration branch for features
215
+ - Feature branches: `feature/<name>` (branch from `develop`)
216
+ - Release branches: `release/<version>` (branch from `develop`)
217
+ - Hotfix branches: `hotfix/<name>` (branch from `main`)
218
+
219
+ ### Release Process
220
+
221
+ **CRITICAL: When creating a new release, ALWAYS:**
222
+
223
+ 1. Update `version` in `package.json`
224
+ 2. Commit the version bump
225
+ 3. Merge `develop` into `main`
226
+ 4. Create annotated tag: `git tag -a vX.Y.Z -m "vX.Y.Z - Description"`
227
+ 5. Push both branches and tag
228
+
229
+ ```bash
230
+ # Example release workflow
231
+ git checkout develop
232
+ # ... make changes ...
233
+ git add . && git commit -m "Your changes"
234
+
235
+ # Update version in package.json
236
+ # Edit package.json: "version": "X.Y.Z"
237
+ git add package.json && git commit -m "Bump version to X.Y.Z"
238
+
239
+ # Merge to main and tag
240
+ git checkout main
241
+ git merge develop
242
+ git tag -a vX.Y.Z -m "vX.Y.Z - Release description"
243
+
244
+ # Push everything
245
+ git push origin main develop
246
+ git push origin vX.Y.Z
247
+ ```
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # opencode-time-tracking
2
+
3
+ Automatic time tracking plugin for OpenCode. Tracks session duration and tool usage, writing entries to a CSV file compatible with Jira worklog sync.
4
+
5
+ ## Installation
6
+
7
+ Add to your `opencode.json`:
8
+
9
+ ```json
10
+ {
11
+ "plugin": ["opencode-time-tracking"]
12
+ }
13
+ ```
14
+
15
+ ## Configuration
16
+
17
+ Create `.opencode/time-tracking.json` in your project:
18
+
19
+ ```json
20
+ {
21
+ "csv_file": "~/time_tracking/time-tracking.csv",
22
+ "user_email": "your@email.com",
23
+ "default_account_key": "YOUR_ACCOUNT_KEY"
24
+ }
25
+ ```
26
+
27
+ ## How it works
28
+
29
+ - Tracks tool executions during each session turn
30
+ - Extracts JIRA ticket from git branch name (e.g., `feature/PROJ-123-description`)
31
+ - Writes CSV entry when session becomes idle (after each complete response)
32
+ - Shows toast notification with tracked time
33
+
34
+ ## CSV Format
35
+
36
+ ```
37
+ 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
38
+ ```
39
+
40
+ ## Events
41
+
42
+ | Event | When triggered |
43
+ |-------|----------------|
44
+ | `session.idle` | After each complete AI response (including all tool calls) |
45
+ | `session.deleted` | When a session is explicitly deleted |
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "opencode-time-tracking",
3
+ "version": "0.1.5",
4
+ "description": "Automatic time tracking plugin for OpenCode - tracks session duration and tool usage to CSV",
5
+ "main": "src/Plugin.ts",
6
+ "types": "src/Plugin.ts",
7
+ "keywords": [
8
+ "opencode",
9
+ "plugin",
10
+ "time-tracking",
11
+ "jira"
12
+ ],
13
+ "author": "TechDivision",
14
+ "license": "MIT",
15
+ "dependencies": {
16
+ "@opencode-ai/plugin": "^1.0.223"
17
+ },
18
+ "devDependencies": {
19
+ "@types/bun": "latest",
20
+ "@types/node": "^25.0.3",
21
+ "typescript": "^5.9.3"
22
+ },
23
+ "peerDependencies": {
24
+ "bun": ">=1.0.0"
25
+ }
26
+ }
package/src/Plugin.ts ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @fileoverview OpenCode Time Tracking Plugin
3
+ *
4
+ * Automatically tracks session duration, tool usage, and token consumption,
5
+ * exporting data to CSV for time tracking integration (e.g., Jira/Tempo).
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ import type { Plugin, Hooks, PluginInput } from "@opencode-ai/plugin"
11
+
12
+ import { ConfigLoader } from "./services/ConfigLoader"
13
+ import { CsvWriter } from "./services/CsvWriter"
14
+ import { SessionManager } from "./services/SessionManager"
15
+ import { TicketExtractor } from "./services/TicketExtractor"
16
+ import { createEventHook } from "./hooks/EventHook"
17
+ import { createToolExecuteAfterHook } from "./hooks/ToolExecuteAfterHook"
18
+
19
+ /**
20
+ * OpenCode Time Tracking Plugin
21
+ *
22
+ * This plugin automatically tracks:
23
+ * - Session duration (start/end time)
24
+ * - Tool usage (which tools were called)
25
+ * - Token consumption (input/output/reasoning tokens)
26
+ * - Ticket references (extracted from user messages or todos)
27
+ *
28
+ * Data is exported to a CSV file configured in `.opencode/time-tracking.json`.
29
+ *
30
+ * @param input - Plugin input containing client, directory, and other context
31
+ * @returns Hooks object with event and tool.execute.after handlers
32
+ *
33
+ * @example
34
+ * ```json
35
+ * // .opencode/time-tracking.json
36
+ * {
37
+ * "csv_file": "~/worklogs/time.csv",
38
+ * "user_email": "user@example.com",
39
+ * "default_account_key": "ACCOUNT-1"
40
+ * }
41
+ * ```
42
+ */
43
+ export const plugin: Plugin = async ({
44
+ client,
45
+ directory,
46
+ }: PluginInput): Promise<Hooks> => {
47
+ const config = await ConfigLoader.load(directory)
48
+
49
+ if (!config) {
50
+ // Silently return empty hooks if no config found
51
+ // Toast notifications don't work during plugin initialization
52
+ return {}
53
+ }
54
+
55
+ const sessionManager = new SessionManager()
56
+ const csvWriter = new CsvWriter(config, directory)
57
+ const ticketExtractor = new TicketExtractor(client)
58
+
59
+ const hooks: Hooks = {
60
+ "tool.execute.after": createToolExecuteAfterHook(
61
+ sessionManager,
62
+ ticketExtractor
63
+ ),
64
+ event: createEventHook(sessionManager, csvWriter, client),
65
+ }
66
+
67
+ return hooks
68
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * @fileoverview Event hook for session lifecycle and token tracking.
3
+ */
4
+
5
+ import type { Event } from "@opencode-ai/sdk"
6
+
7
+ import type { CsvWriter } from "../services/CsvWriter"
8
+ import type { SessionManager } from "../services/SessionManager"
9
+ import type { MessagePartUpdatedProperties } from "../types/MessagePartUpdatedProperties"
10
+ import type { MessageWithParts } from "../types/MessageWithParts"
11
+ import type { OpencodeClient } from "../types/OpencodeClient"
12
+
13
+ import { DescriptionGenerator } from "../utils/DescriptionGenerator"
14
+
15
+ /**
16
+ * Extracts the summary title from the last user message.
17
+ *
18
+ * @param client - The OpenCode SDK client
19
+ * @param sessionID - The session identifier
20
+ * @returns The summary title, or `null` if not found
21
+ *
22
+ * @internal
23
+ */
24
+ async function extractSummaryTitle(
25
+ client: OpencodeClient,
26
+ sessionID: string
27
+ ): Promise<string | null> {
28
+ try {
29
+ const result = await client.session.messages({
30
+ path: { id: sessionID },
31
+ } as Parameters<typeof client.session.messages>[0])
32
+
33
+ if (!result.data) {
34
+ return null
35
+ }
36
+
37
+ const messages = result.data as MessageWithParts[]
38
+
39
+ // Find the last user message with a summary title
40
+ for (let i = messages.length - 1; i >= 0; i--) {
41
+ const message = messages[i]
42
+
43
+ if (message.info.role === "user" && message.info.summary?.title) {
44
+ return message.info.summary.title
45
+ }
46
+ }
47
+
48
+ return null
49
+ } catch {
50
+ return null
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Creates the event hook for session lifecycle management.
56
+ *
57
+ * @param sessionManager - The session manager instance
58
+ * @param csvWriter - The CSV writer instance
59
+ * @param client - The OpenCode SDK client
60
+ * @returns The event hook function
61
+ *
62
+ * @remarks
63
+ * Handles two types of events:
64
+ *
65
+ * 1. **message.part.updated** - Tracks token usage from step-finish parts
66
+ * 2. **session.idle** - Finalizes and exports the session
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * const hooks: Hooks = {
71
+ * event: createEventHook(sessionManager, csvWriter, client),
72
+ * }
73
+ * ```
74
+ */
75
+ export function createEventHook(
76
+ sessionManager: SessionManager,
77
+ csvWriter: CsvWriter,
78
+ client: OpencodeClient
79
+ ) {
80
+ return async ({ event }: { event: Event }): Promise<void> => {
81
+ // Track token usage from step-finish events
82
+ if (event.type === "message.part.updated") {
83
+ const props = event.properties as MessagePartUpdatedProperties
84
+ const part = props.part
85
+
86
+ if (part.type === "step-finish" && part.sessionID && part.tokens) {
87
+ sessionManager.addTokenUsage(part.sessionID, {
88
+ input: part.tokens.input,
89
+ output: part.tokens.output,
90
+ reasoning: part.tokens.reasoning,
91
+ cacheRead: part.tokens.cache.read,
92
+ cacheWrite: part.tokens.cache.write,
93
+ })
94
+ }
95
+
96
+ return
97
+ }
98
+
99
+ // Handle session idle events (only log on idle, not on deleted)
100
+ if (event.type === "session.idle") {
101
+ const props = event.properties as { sessionID?: string }
102
+ const sessionID = props.sessionID
103
+
104
+ if (!sessionID) {
105
+ return
106
+ }
107
+
108
+ const session = sessionManager.get(sessionID)
109
+
110
+ if (!session || session.activities.length === 0) {
111
+ sessionManager.delete(sessionID)
112
+ return
113
+ }
114
+
115
+ const endTime = Date.now()
116
+ const durationSeconds = Math.round((endTime - session.startTime) / 1000)
117
+
118
+ // Try to get summary title from messages, fallback to generated description
119
+ const summaryTitle = await extractSummaryTitle(client, sessionID)
120
+ const description =
121
+ summaryTitle || DescriptionGenerator.generate(session.activities)
122
+
123
+ const toolSummary = DescriptionGenerator.generateToolSummary(
124
+ session.activities
125
+ )
126
+
127
+ const totalTokens =
128
+ session.tokenUsage.input +
129
+ session.tokenUsage.output +
130
+ session.tokenUsage.reasoning
131
+
132
+ try {
133
+ await csvWriter.write({
134
+ ticket: session.ticket,
135
+ startTime: session.startTime,
136
+ endTime,
137
+ durationSeconds,
138
+ description,
139
+ notes: `Auto-tracked: ${toolSummary}`,
140
+ tokenUsage: session.tokenUsage,
141
+ })
142
+
143
+ const minutes = Math.round(durationSeconds / 60)
144
+
145
+ await client.tui.showToast({
146
+ body: {
147
+ message: `Time tracked: ${minutes} min, ${totalTokens} tokens${session.ticket ? ` for ${session.ticket}` : ""}`,
148
+ variant: "success",
149
+ },
150
+ })
151
+ } catch {
152
+ await client.tui.showToast({
153
+ body: {
154
+ message: "Time Tracking: Failed to save entry",
155
+ variant: "error",
156
+ },
157
+ })
158
+ }
159
+
160
+ sessionManager.delete(sessionID)
161
+ }
162
+ }
163
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @fileoverview Hook for tracking tool executions.
3
+ */
4
+
5
+ import type { SessionManager } from "../services/SessionManager"
6
+ import type { TicketExtractor } from "../services/TicketExtractor"
7
+ import type { ToolExecuteAfterInput } from "../types/ToolExecuteAfterInput"
8
+ import type { ToolExecuteAfterOutput } from "../types/ToolExecuteAfterOutput"
9
+
10
+ /**
11
+ * Creates the tool.execute.after hook for activity tracking.
12
+ *
13
+ * @param sessionManager - The session manager instance
14
+ * @param ticketExtractor - The ticket extractor instance
15
+ * @returns The hook function
16
+ *
17
+ * @remarks
18
+ * This hook is called after every tool execution and:
19
+ *
20
+ * 1. Creates a new session if one doesn't exist
21
+ * 2. Extracts and updates the ticket reference from context
22
+ * 3. Records the tool activity with timestamp and file info
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const hooks: Hooks = {
27
+ * "tool.execute.after": createToolExecuteAfterHook(sessionManager, ticketExtractor),
28
+ * }
29
+ * ```
30
+ */
31
+ export function createToolExecuteAfterHook(
32
+ sessionManager: SessionManager,
33
+ ticketExtractor: TicketExtractor
34
+ ) {
35
+ return async (
36
+ input: ToolExecuteAfterInput,
37
+ output: ToolExecuteAfterOutput
38
+ ): Promise<void> => {
39
+ const { tool, sessionID } = input
40
+ const { title, metadata } = output
41
+
42
+ // Create session if it doesn't exist
43
+ if (!sessionManager.has(sessionID)) {
44
+ sessionManager.create(sessionID, null)
45
+ }
46
+
47
+ // Extract and update ticket on every tool call
48
+ const ticket = await ticketExtractor.extract(sessionID)
49
+ sessionManager.updateTicket(sessionID, ticket)
50
+
51
+ // Extract file info from metadata
52
+ let file: string | undefined
53
+
54
+ if (metadata) {
55
+ const meta = metadata as Record<string, unknown>
56
+
57
+ file = (meta.filePath || meta.filepath || meta.file) as
58
+ | string
59
+ | undefined
60
+
61
+ if (!file && meta.filediff) {
62
+ file = (meta.filediff as Record<string, unknown>).file as
63
+ | string
64
+ | undefined
65
+ }
66
+ }
67
+
68
+ if (!file && title) {
69
+ file = title
70
+ }
71
+
72
+ sessionManager.addActivity(sessionID, {
73
+ tool,
74
+ timestamp: Date.now(),
75
+ file,
76
+ })
77
+ }
78
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @fileoverview Configuration loader for the time tracking plugin.
3
+ */
4
+
5
+ import type { TimeTrackingConfig } from "../types/TimeTrackingConfig"
6
+
7
+ import "../types/Bun"
8
+
9
+ /**
10
+ * Loads the plugin configuration from the project directory.
11
+ *
12
+ * @remarks
13
+ * The configuration file is expected at `.opencode/time-tracking.json`
14
+ * within the project directory.
15
+ */
16
+ export class ConfigLoader {
17
+ /**
18
+ * Loads the time tracking configuration from the filesystem.
19
+ *
20
+ * @param directory - The project directory path
21
+ * @returns The configuration object, or `null` if not found or invalid
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * const config = await ConfigLoader.load("/path/to/project")
26
+ * if (config) {
27
+ * console.log(config.csv_file)
28
+ * }
29
+ * ```
30
+ */
31
+ static async load(directory: string): Promise<TimeTrackingConfig | null> {
32
+ const configPath = `${directory}/.opencode/time-tracking.json`
33
+
34
+ try {
35
+ const file = Bun.file(configPath)
36
+
37
+ if (await file.exists()) {
38
+ return (await file.json()) as TimeTrackingConfig
39
+ }
40
+
41
+ return null
42
+ } catch {
43
+ return null
44
+ }
45
+ }
46
+ }