@techdivision/opencode-time-tracking 0.6.1 → 0.7.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/CHANGELOG.md +20 -0
- package/README.md +44 -1
- package/package.json +1 -1
- package/src/Plugin.ts +1 -1
- package/src/hooks/EventHook.ts +5 -2
- package/src/services/TicketExtractor.ts +32 -5
- package/src/services/TicketResolver.ts +37 -8
- package/src/types/TimeTrackingConfig.ts +10 -0
- package/src/utils/AgentMatcher.ts +31 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.7.1] - 2025-03-01
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fix `agent_defaults` lookup to be tolerant of `@` prefix in agent names
|
|
10
|
+
- Add `AgentMatcher` utility for consistent agent name normalization across the codebase
|
|
11
|
+
|
|
12
|
+
## [0.7.0] - 2025-02-04
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Add `valid_projects` configuration for JIRA project whitelist
|
|
17
|
+
- Restrict ticket detection to specific projects when configured
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Default ticket pattern now requires at least 2 uppercase letters
|
|
22
|
+
- Matches: `PROJ-123`, `SOSO-1`, `AB-99`
|
|
23
|
+
- Does not match: `V-1`, `X-9` (single letter), `UTF-8` (false positive)
|
|
24
|
+
|
|
5
25
|
## [0.6.1] - 2025-02-02
|
|
6
26
|
|
|
7
27
|
### Fixed
|
package/README.md
CHANGED
|
@@ -100,6 +100,20 @@ Skip time tracking for specific agents:
|
|
|
100
100
|
}
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
+
#### Project Whitelist
|
|
104
|
+
|
|
105
|
+
Restrict ticket detection to specific JIRA projects:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"time_tracking": {
|
|
110
|
+
"csv_file": "...",
|
|
111
|
+
"global_default": { ... },
|
|
112
|
+
"valid_projects": ["PROJ", "SOSO", "FEAT"]
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
103
117
|
### Full Example
|
|
104
118
|
|
|
105
119
|
```json
|
|
@@ -120,11 +134,40 @@ Skip time tracking for specific agents:
|
|
|
120
134
|
"issue_key": "PROJ-REVIEW"
|
|
121
135
|
}
|
|
122
136
|
},
|
|
123
|
-
"ignored_agents": ["@internal"]
|
|
137
|
+
"ignored_agents": ["@internal"],
|
|
138
|
+
"valid_projects": ["PROJ", "SOSO"]
|
|
124
139
|
}
|
|
125
140
|
}
|
|
126
141
|
```
|
|
127
142
|
|
|
143
|
+
## Ticket Detection
|
|
144
|
+
|
|
145
|
+
### Pattern
|
|
146
|
+
|
|
147
|
+
By default, tickets must have at least 2 uppercase letters followed by a number:
|
|
148
|
+
- Matches: `PROJ-123`, `SOSO-1`, `AB-99`
|
|
149
|
+
- Does not match: `V-1`, `X-9` (single letter), `UTF-8` (common false positive)
|
|
150
|
+
|
|
151
|
+
### Project Whitelist
|
|
152
|
+
|
|
153
|
+
When `valid_projects` is configured, only tickets from those projects are recognized:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"time_tracking": {
|
|
158
|
+
"valid_projects": ["PROJ", "SOSO", "FEAT"]
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
With whitelist:
|
|
164
|
+
- Matches: `PROJ-123`, `SOSO-1`, `FEAT-99`
|
|
165
|
+
- Does not match: `UTF-8`, `ISO-9001`, `OTHER-123`
|
|
166
|
+
|
|
167
|
+
Without whitelist (default):
|
|
168
|
+
- Matches any pattern with 2+ uppercase letters: `PROJ-123`, `AB-1`
|
|
169
|
+
- Does not match single-letter prefixes: `V-1`, `X-99`
|
|
170
|
+
|
|
128
171
|
## Fallback Hierarchy
|
|
129
172
|
|
|
130
173
|
### Ticket Resolution
|
package/package.json
CHANGED
package/src/Plugin.ts
CHANGED
|
@@ -61,7 +61,7 @@ export const plugin: Plugin = async ({
|
|
|
61
61
|
|
|
62
62
|
const sessionManager = new SessionManager()
|
|
63
63
|
const csvWriter = new CsvWriter(config, directory)
|
|
64
|
-
const ticketExtractor = new TicketExtractor(client)
|
|
64
|
+
const ticketExtractor = new TicketExtractor(client, config.valid_projects)
|
|
65
65
|
const ticketResolver = new TicketResolver(config, ticketExtractor)
|
|
66
66
|
|
|
67
67
|
const hooks: Hooks = {
|
package/src/hooks/EventHook.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { MessageWithParts } from "../types/MessageWithParts"
|
|
|
12
12
|
import type { OpencodeClient } from "../types/OpencodeClient"
|
|
13
13
|
import type { TimeTrackingConfig } from "../types/TimeTrackingConfig"
|
|
14
14
|
|
|
15
|
+
import { AgentMatcher } from "../utils/AgentMatcher"
|
|
15
16
|
import { DescriptionGenerator } from "../utils/DescriptionGenerator"
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -193,9 +194,11 @@ export function createEventHook(
|
|
|
193
194
|
const agentString = session.agent?.name ?? null
|
|
194
195
|
|
|
195
196
|
// Check if agent should be ignored (tolerant matching: with or without @ prefix)
|
|
196
|
-
const normalizedAgent = agentString
|
|
197
|
+
const normalizedAgent = agentString
|
|
198
|
+
? AgentMatcher.normalize(agentString)
|
|
199
|
+
: null
|
|
197
200
|
const isIgnoredAgent = config.ignored_agents?.some(
|
|
198
|
-
(ignored) =>
|
|
201
|
+
(ignored) => AgentMatcher.normalize(ignored) === normalizedAgent
|
|
199
202
|
)
|
|
200
203
|
|
|
201
204
|
if (agentString && isIgnoredAgent) {
|
|
@@ -7,10 +7,12 @@ import type { OpencodeClient } from "../types/OpencodeClient"
|
|
|
7
7
|
import type { Todo } from "../types/Todo"
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Default regular expression pattern for Jira ticket references.
|
|
11
|
+
* Requires at least 2 uppercase letters followed by a dash and digits.
|
|
12
|
+
* Matches patterns like "PROJ-123", "AB-1", "FEATURE-9999".
|
|
13
|
+
* Does not match single-letter prefixes like "V-1" or "X-99".
|
|
12
14
|
*/
|
|
13
|
-
const
|
|
15
|
+
const DEFAULT_TICKET_PATTERN = /\b([A-Z]{2,}-\d+)\b/
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Extracts Jira ticket references from user messages and todos.
|
|
@@ -22,18 +24,43 @@ const TICKET_PATTERN = /([A-Z]+-\d+)/
|
|
|
22
24
|
*
|
|
23
25
|
* Returns the first match found, allowing tickets to be updated
|
|
24
26
|
* when mentioned in later messages.
|
|
27
|
+
*
|
|
28
|
+
* If `validProjects` is provided, only tickets from those projects
|
|
29
|
+
* are recognized. Otherwise, any ticket matching the default pattern
|
|
30
|
+
* (2+ uppercase letters) is accepted.
|
|
25
31
|
*/
|
|
26
32
|
export class TicketExtractor {
|
|
27
33
|
/** OpenCode SDK client */
|
|
28
34
|
private client: OpencodeClient
|
|
29
35
|
|
|
36
|
+
/** Compiled regex pattern for ticket matching */
|
|
37
|
+
private ticketPattern: RegExp
|
|
38
|
+
|
|
30
39
|
/**
|
|
31
40
|
* Creates a new ticket extractor instance.
|
|
32
41
|
*
|
|
33
42
|
* @param client - The OpenCode SDK client
|
|
43
|
+
* @param validProjects - Optional whitelist of valid JIRA project keys
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* // Accept any ticket with 2+ letter prefix
|
|
48
|
+
* const extractor = new TicketExtractor(client)
|
|
49
|
+
*
|
|
50
|
+
* // Only accept PROJ and SOSO tickets
|
|
51
|
+
* const extractor = new TicketExtractor(client, ["PROJ", "SOSO"])
|
|
52
|
+
* ```
|
|
34
53
|
*/
|
|
35
|
-
constructor(client: OpencodeClient) {
|
|
54
|
+
constructor(client: OpencodeClient, validProjects?: string[]) {
|
|
36
55
|
this.client = client
|
|
56
|
+
|
|
57
|
+
if (validProjects && validProjects.length > 0) {
|
|
58
|
+
// Build pattern that only matches specified projects
|
|
59
|
+
const projectsRegex = validProjects.join("|")
|
|
60
|
+
this.ticketPattern = new RegExp(`\\b((?:${projectsRegex})-\\d+)\\b`)
|
|
61
|
+
} else {
|
|
62
|
+
this.ticketPattern = DEFAULT_TICKET_PATTERN
|
|
63
|
+
}
|
|
37
64
|
}
|
|
38
65
|
|
|
39
66
|
/**
|
|
@@ -149,7 +176,7 @@ export class TicketExtractor {
|
|
|
149
176
|
* @returns The first ticket match, or `null` if not found
|
|
150
177
|
*/
|
|
151
178
|
private extractFromText(text: string): string | null {
|
|
152
|
-
const match = text.match(
|
|
179
|
+
const match = text.match(this.ticketPattern)
|
|
153
180
|
|
|
154
181
|
return match?.[1] ?? null
|
|
155
182
|
}
|
|
@@ -6,6 +6,8 @@ import type { ResolvedTicketInfo } from "../types/ResolvedTicketInfo"
|
|
|
6
6
|
import type { TimeTrackingConfig } from "../types/TimeTrackingConfig"
|
|
7
7
|
import type { TicketExtractor } from "./TicketExtractor"
|
|
8
8
|
|
|
9
|
+
import { AgentMatcher } from "../utils/AgentMatcher"
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
12
|
* Resolves tickets and account keys using fallback hierarchy.
|
|
11
13
|
*
|
|
@@ -65,11 +67,13 @@ export class TicketResolver {
|
|
|
65
67
|
}
|
|
66
68
|
}
|
|
67
69
|
|
|
68
|
-
// 2. Try agent default
|
|
69
|
-
|
|
70
|
+
// 2. Try agent default (tolerant matching: with or without @ prefix)
|
|
71
|
+
const agentKey = agentName ? this.findAgentKey(agentName) : null
|
|
72
|
+
|
|
73
|
+
if (agentKey) {
|
|
70
74
|
return {
|
|
71
|
-
ticket: this.config.agent_defaults[
|
|
72
|
-
accountKey: this.resolveAccountKey(
|
|
75
|
+
ticket: this.config.agent_defaults![agentKey].issue_key,
|
|
76
|
+
accountKey: this.resolveAccountKey(agentKey),
|
|
73
77
|
}
|
|
74
78
|
}
|
|
75
79
|
|
|
@@ -88,16 +92,41 @@ export class TicketResolver {
|
|
|
88
92
|
}
|
|
89
93
|
}
|
|
90
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Finds the matching config key for an agent name.
|
|
97
|
+
*
|
|
98
|
+
* @param agentName - The agent name from the SDK
|
|
99
|
+
* @returns The matching config key, or `null` if not found
|
|
100
|
+
*
|
|
101
|
+
* @remarks
|
|
102
|
+
* Normalizes both the agent name and config keys to ensure
|
|
103
|
+
* matching works regardless of @ prefix.
|
|
104
|
+
*/
|
|
105
|
+
private findAgentKey(agentName: string): string | null {
|
|
106
|
+
const defaults = this.config.agent_defaults
|
|
107
|
+
|
|
108
|
+
if (!defaults) {
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const normalized = AgentMatcher.normalize(agentName)
|
|
113
|
+
const key = Object.keys(defaults).find(
|
|
114
|
+
(k) => AgentMatcher.normalize(k) === normalized
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return key ?? null
|
|
118
|
+
}
|
|
119
|
+
|
|
91
120
|
/**
|
|
92
121
|
* Resolves account key using fallback hierarchy.
|
|
93
122
|
*
|
|
94
|
-
* @param
|
|
123
|
+
* @param agentKey - The agent config key, or `null`
|
|
95
124
|
* @returns Resolved Tempo account key
|
|
96
125
|
*/
|
|
97
|
-
private resolveAccountKey(
|
|
126
|
+
private resolveAccountKey(agentKey: string | null): string {
|
|
98
127
|
// 1. Agent-specific account_key
|
|
99
|
-
if (
|
|
100
|
-
return this.config.agent_defaults[
|
|
128
|
+
if (agentKey && this.config.agent_defaults?.[agentKey]?.account_key) {
|
|
129
|
+
return this.config.agent_defaults[agentKey].account_key!
|
|
101
130
|
}
|
|
102
131
|
|
|
103
132
|
// 2. Global default account_key (required)
|
|
@@ -51,6 +51,16 @@ export interface TimeTrackingJsonConfig {
|
|
|
51
51
|
* Agent names should include the "@" prefix (e.g., "@internal").
|
|
52
52
|
*/
|
|
53
53
|
ignored_agents?: string[]
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Whitelist of valid JIRA project keys.
|
|
57
|
+
*
|
|
58
|
+
* @remarks
|
|
59
|
+
* If set, only tickets from these projects are recognized.
|
|
60
|
+
* If not set, any ticket matching the default pattern is accepted.
|
|
61
|
+
* Project keys should be uppercase with at least 2 letters (e.g., "PROJ", "SOSO").
|
|
62
|
+
*/
|
|
63
|
+
valid_projects?: string[]
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
/**
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Utility for normalizing agent names.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes agent names to a canonical form.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* Agent names can appear with or without the "@" prefix
|
|
10
|
+
* depending on the source (SDK, config, user input).
|
|
11
|
+
* This class ensures consistent comparison by normalizing
|
|
12
|
+
* all agent names to the "@<name>" format.
|
|
13
|
+
*/
|
|
14
|
+
export class AgentMatcher {
|
|
15
|
+
/**
|
|
16
|
+
* Normalizes an agent name to canonical form (with @ prefix).
|
|
17
|
+
*
|
|
18
|
+
* @param agentName - The agent name to normalize
|
|
19
|
+
* @returns The normalized agent name with @ prefix
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* AgentMatcher.normalize("developer") // → "@developer"
|
|
24
|
+
* AgentMatcher.normalize("@developer") // → "@developer"
|
|
25
|
+
* AgentMatcher.normalize("@time-tracking") // → "@time-tracking"
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
static normalize(agentName: string): string {
|
|
29
|
+
return agentName.startsWith("@") ? agentName : `@${agentName}`
|
|
30
|
+
}
|
|
31
|
+
}
|