@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techdivision/opencode-time-tracking",
3
- "version": "0.6.1",
3
+ "version": "0.7.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
@@ -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 = {
@@ -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?.replace(/^@/, "")
197
+ const normalizedAgent = agentString
198
+ ? AgentMatcher.normalize(agentString)
199
+ : null
197
200
  const isIgnoredAgent = config.ignored_agents?.some(
198
- (ignored) => ignored.replace(/^@/, "") === normalizedAgent
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
- * Regular expression pattern for Jira ticket references.
11
- * Matches patterns like "PROJ-123", "ABC-1", "FEATURE-9999".
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 TICKET_PATTERN = /([A-Z]+-\d+)/
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(TICKET_PATTERN)
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
- if (agentName && this.config.agent_defaults?.[agentName]) {
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[agentName].issue_key,
72
- accountKey: this.resolveAccountKey(agentName),
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 agentName - The agent name, or `null`
123
+ * @param agentKey - The agent config key, or `null`
95
124
  * @returns Resolved Tempo account key
96
125
  */
97
- private resolveAccountKey(agentName: string | null): string {
126
+ private resolveAccountKey(agentKey: string | null): string {
98
127
  // 1. Agent-specific account_key
99
- if (agentName && this.config.agent_defaults?.[agentName]?.account_key) {
100
- return this.config.agent_defaults[agentName].account_key!
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
+ }