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 +247 -0
- package/README.md +45 -0
- package/package.json +26 -0
- package/src/Plugin.ts +68 -0
- package/src/hooks/EventHook.ts +163 -0
- package/src/hooks/ToolExecuteAfterHook.ts +78 -0
- package/src/services/ConfigLoader.ts +46 -0
- package/src/services/CsvWriter.ts +136 -0
- package/src/services/SessionManager.ts +129 -0
- package/src/services/TicketExtractor.ts +156 -0
- package/src/types/ActivityData.ts +17 -0
- package/src/types/Bun.ts +40 -0
- package/src/types/CsvEntryData.ts +31 -0
- package/src/types/MessageInfo.ts +16 -0
- package/src/types/MessagePart.ts +14 -0
- package/src/types/MessagePartUpdatedProperties.ts +22 -0
- package/src/types/MessageSummary.ts +14 -0
- package/src/types/MessageWithParts.ts +17 -0
- package/src/types/OpencodeClient.ts +14 -0
- package/src/types/SessionData.ts +23 -0
- package/src/types/StepFinishPart.ts +39 -0
- package/src/types/TimeTrackingConfig.ts +25 -0
- package/src/types/Todo.ts +11 -0
- package/src/types/TokenUsage.ts +23 -0
- package/src/types/ToolExecuteAfterInput.ts +17 -0
- package/src/types/ToolExecuteAfterOutput.ts +17 -0
- package/src/utils/CsvFormatter.ts +57 -0
- package/src/utils/DescriptionGenerator.ts +119 -0
- package/tsconfig.json +13 -0
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
|
+
}
|