prjct-cli 0.40.0 → 0.41.0

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.
@@ -0,0 +1,57 @@
1
+ /**
2
+ * JIRA Cache Module
3
+ * 5-minute TTL cache for JIRA API responses to reduce API calls
4
+ */
5
+
6
+ import { TTLCache } from '../../utils/cache'
7
+ import type { Issue } from '../issue-tracker/types'
8
+
9
+ // 5-minute TTL for JIRA API responses
10
+ const JIRA_CACHE_TTL = 5 * 60 * 1000 // 300000ms
11
+
12
+ /**
13
+ * Cache for individual issues (by key like "ENG-123" or ID)
14
+ * Key format: "issue:{key}" or "issue:{id}"
15
+ */
16
+ export const issueCache = new TTLCache<Issue>({
17
+ ttl: JIRA_CACHE_TTL,
18
+ maxSize: 100,
19
+ })
20
+
21
+ /**
22
+ * Cache for assigned issues list
23
+ * Key format: "assigned:{userId}" or "assigned:me"
24
+ */
25
+ export const assignedIssuesCache = new TTLCache<Issue[]>({
26
+ ttl: JIRA_CACHE_TTL,
27
+ maxSize: 10,
28
+ })
29
+
30
+ /**
31
+ * Cache for projects list
32
+ * Key format: "projects"
33
+ */
34
+ export const projectsCache = new TTLCache<Array<{ id: string; name: string; key?: string }>>({
35
+ ttl: JIRA_CACHE_TTL,
36
+ maxSize: 5,
37
+ })
38
+
39
+ /**
40
+ * Clear all JIRA caches
41
+ */
42
+ export function clearJiraCache(): void {
43
+ issueCache.clear()
44
+ assignedIssuesCache.clear()
45
+ projectsCache.clear()
46
+ }
47
+
48
+ /**
49
+ * Get cache statistics for debugging
50
+ */
51
+ export function getJiraCacheStats() {
52
+ return {
53
+ issues: issueCache.stats(),
54
+ assignedIssues: assignedIssuesCache.stats(),
55
+ projects: projectsCache.stats(),
56
+ }
57
+ }
@@ -1,25 +1,30 @@
1
1
  /**
2
2
  * JIRA Integration
3
3
  *
4
- * Provides JIRA issue tracking integration for prjct-cli.
4
+ * Provides JIRA issue tracking integration for prjct-cli using REST API.
5
5
  *
6
- * Two authentication modes:
7
- *
8
- * 1. API Token Mode (direct REST API):
6
+ * Authentication (API Token Mode):
9
7
  * - JIRA_BASE_URL: Your JIRA instance URL (e.g., https://company.atlassian.net)
10
8
  * - JIRA_EMAIL: Your Atlassian account email
11
9
  * - JIRA_API_TOKEN: API token from https://id.atlassian.com/manage-profile/security/api-tokens
12
- *
13
- * 2. MCP Mode (for corporate SSO):
14
- * - No environment variables needed
15
- * - Requires Atlassian MCP server configured in ~/.claude/mcp.json
16
- * - Authenticates via browser (OAuth 2.1, SSO compatible)
17
10
  */
18
11
 
19
- // REST API client (supports both API token and MCP modes)
12
+ // REST API client
20
13
  export { JiraProvider, jiraProvider, type JiraAuthMode } from './client'
21
14
 
22
- // MCP adapter for generating Claude instructions
15
+ // Service layer with caching (preferred API)
16
+ export { JiraService, jiraService } from './service'
17
+
18
+ // Cache utilities
19
+ export {
20
+ issueCache,
21
+ assignedIssuesCache,
22
+ projectsCache,
23
+ clearJiraCache,
24
+ getJiraCacheStats,
25
+ } from './cache'
26
+
27
+ // MCP adapter (deprecated - will be removed)
23
28
  export {
24
29
  JiraMCPAdapter,
25
30
  jiraMCPAdapter,
@@ -0,0 +1,244 @@
1
+ /**
2
+ * JIRA Service Layer
3
+ * Wraps JiraProvider with caching for improved performance.
4
+ * All operations are cached with 5-minute TTL.
5
+ */
6
+
7
+ import { jiraProvider } from './client'
8
+ import {
9
+ issueCache,
10
+ assignedIssuesCache,
11
+ projectsCache,
12
+ clearJiraCache,
13
+ getJiraCacheStats,
14
+ } from './cache'
15
+ import type {
16
+ Issue,
17
+ CreateIssueInput,
18
+ UpdateIssueInput,
19
+ FetchOptions,
20
+ JiraConfig,
21
+ } from '../issue-tracker/types'
22
+
23
+ export class JiraService {
24
+ private initialized = false
25
+ private userId: string | null = null
26
+
27
+ /**
28
+ * Check if service is ready
29
+ */
30
+ isReady(): boolean {
31
+ return this.initialized && jiraProvider.isConfigured()
32
+ }
33
+
34
+ /**
35
+ * Initialize the service with config
36
+ * Must be called before any operations
37
+ */
38
+ async initialize(config: JiraConfig): Promise<void> {
39
+ if (this.initialized) return
40
+
41
+ await jiraProvider.initialize(config)
42
+ this.initialized = true
43
+ }
44
+
45
+ /**
46
+ * Initialize from credentials directly
47
+ * Convenience method for simple setup
48
+ */
49
+ async initializeFromCredentials(
50
+ baseUrl: string,
51
+ email: string,
52
+ apiToken: string,
53
+ projectKey?: string
54
+ ): Promise<void> {
55
+ // Set env vars for the provider
56
+ process.env.JIRA_BASE_URL = baseUrl
57
+ process.env.JIRA_EMAIL = email
58
+ process.env.JIRA_API_TOKEN = apiToken
59
+
60
+ const config: JiraConfig = {
61
+ enabled: true,
62
+ provider: 'jira',
63
+ baseUrl,
64
+ projectKey,
65
+ syncOn: { task: true, done: true, ship: true },
66
+ enrichment: { enabled: true, updateProvider: true },
67
+ }
68
+ await this.initialize(config)
69
+ }
70
+
71
+ /**
72
+ * Get issues assigned to current user (cached)
73
+ */
74
+ async fetchAssignedIssues(options?: FetchOptions): Promise<Issue[]> {
75
+ this.ensureInitialized()
76
+
77
+ const cacheKey = `assigned:${this.userId || 'me'}`
78
+ const cached = assignedIssuesCache.get(cacheKey)
79
+ if (cached) {
80
+ return cached
81
+ }
82
+
83
+ const issues = await jiraProvider.fetchAssignedIssues(options)
84
+ assignedIssuesCache.set(cacheKey, issues)
85
+
86
+ // Also cache individual issues
87
+ for (const issue of issues) {
88
+ issueCache.set(`issue:${issue.id}`, issue)
89
+ issueCache.set(`issue:${issue.externalId}`, issue)
90
+ }
91
+
92
+ return issues
93
+ }
94
+
95
+ /**
96
+ * Get issues from a project (cached)
97
+ */
98
+ async fetchProjectIssues(projectKey: string, options?: FetchOptions): Promise<Issue[]> {
99
+ this.ensureInitialized()
100
+
101
+ const cacheKey = `project:${projectKey}`
102
+ const cached = assignedIssuesCache.get(cacheKey)
103
+ if (cached) {
104
+ return cached
105
+ }
106
+
107
+ const issues = await jiraProvider.fetchTeamIssues(projectKey, options)
108
+ assignedIssuesCache.set(cacheKey, issues)
109
+
110
+ // Also cache individual issues
111
+ for (const issue of issues) {
112
+ issueCache.set(`issue:${issue.id}`, issue)
113
+ issueCache.set(`issue:${issue.externalId}`, issue)
114
+ }
115
+
116
+ return issues
117
+ }
118
+
119
+ /**
120
+ * Get a single issue by key (like "ENG-123") or ID (cached)
121
+ */
122
+ async fetchIssue(id: string): Promise<Issue | null> {
123
+ this.ensureInitialized()
124
+
125
+ // Check cache first
126
+ const cacheKey = `issue:${id}`
127
+ const cached = issueCache.get(cacheKey)
128
+ if (cached) {
129
+ return cached
130
+ }
131
+
132
+ const issue = await jiraProvider.fetchIssue(id)
133
+ if (issue) {
134
+ // Cache by both ID and key
135
+ issueCache.set(`issue:${issue.id}`, issue)
136
+ issueCache.set(`issue:${issue.externalId}`, issue)
137
+ }
138
+
139
+ return issue
140
+ }
141
+
142
+ /**
143
+ * Create a new issue (invalidates assigned cache)
144
+ */
145
+ async createIssue(input: CreateIssueInput): Promise<Issue> {
146
+ this.ensureInitialized()
147
+
148
+ const issue = await jiraProvider.createIssue(input)
149
+
150
+ // Cache the new issue
151
+ issueCache.set(`issue:${issue.id}`, issue)
152
+ issueCache.set(`issue:${issue.externalId}`, issue)
153
+
154
+ // Invalidate assigned issues cache (new issue may be assigned)
155
+ assignedIssuesCache.clear()
156
+
157
+ return issue
158
+ }
159
+
160
+ /**
161
+ * Update an issue (invalidates cache for that issue)
162
+ */
163
+ async updateIssue(id: string, input: UpdateIssueInput): Promise<Issue> {
164
+ this.ensureInitialized()
165
+
166
+ const issue = await jiraProvider.updateIssue(id, input)
167
+
168
+ // Update cache
169
+ issueCache.set(`issue:${issue.id}`, issue)
170
+ issueCache.set(`issue:${issue.externalId}`, issue)
171
+
172
+ return issue
173
+ }
174
+
175
+ /**
176
+ * Mark issue as in progress (invalidates cache)
177
+ */
178
+ async markInProgress(id: string): Promise<void> {
179
+ this.ensureInitialized()
180
+
181
+ await jiraProvider.markInProgress(id)
182
+
183
+ // Invalidate caches
184
+ issueCache.delete(`issue:${id}`)
185
+ assignedIssuesCache.clear()
186
+ }
187
+
188
+ /**
189
+ * Mark issue as done (invalidates cache)
190
+ */
191
+ async markDone(id: string): Promise<void> {
192
+ this.ensureInitialized()
193
+
194
+ await jiraProvider.markDone(id)
195
+
196
+ // Invalidate caches
197
+ issueCache.delete(`issue:${id}`)
198
+ assignedIssuesCache.clear()
199
+ }
200
+
201
+ /**
202
+ * Get available projects (cached)
203
+ */
204
+ async getProjects(): Promise<Array<{ id: string; name: string; key?: string }>> {
205
+ this.ensureInitialized()
206
+
207
+ const cached = projectsCache.get('projects')
208
+ if (cached) {
209
+ return cached
210
+ }
211
+
212
+ const projects = await jiraProvider.getTeams()
213
+ projectsCache.set('projects', projects)
214
+ return projects
215
+ }
216
+
217
+ /**
218
+ * Clear all caches
219
+ */
220
+ clearCache(): void {
221
+ clearJiraCache()
222
+ }
223
+
224
+ /**
225
+ * Get cache statistics for debugging
226
+ */
227
+ getCacheStats() {
228
+ return getJiraCacheStats()
229
+ }
230
+
231
+ /**
232
+ * Ensure service is initialized
233
+ */
234
+ private ensureInitialized(): void {
235
+ if (!this.initialized) {
236
+ throw new Error(
237
+ 'JIRA service not initialized. Call jiraService.initialize() first or run `p. jira setup`.'
238
+ )
239
+ }
240
+ }
241
+ }
242
+
243
+ // Singleton instance
244
+ export const jiraService = new JiraService()
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Linear Cache Module
3
+ * 5-minute TTL cache for Linear API responses to reduce API calls
4
+ */
5
+
6
+ import { TTLCache } from '../../utils/cache'
7
+ import type { Issue } from '../issue-tracker/types'
8
+
9
+ // 5-minute TTL for Linear API responses
10
+ const LINEAR_CACHE_TTL = 5 * 60 * 1000 // 300000ms
11
+
12
+ /**
13
+ * Cache for individual issues (by ID or identifier)
14
+ * Key format: "issue:{id}" or "issue:{identifier}"
15
+ */
16
+ export const issueCache = new TTLCache<Issue>({
17
+ ttl: LINEAR_CACHE_TTL,
18
+ maxSize: 100,
19
+ })
20
+
21
+ /**
22
+ * Cache for assigned issues list
23
+ * Key format: "assigned:{userId}" or "assigned:me"
24
+ */
25
+ export const assignedIssuesCache = new TTLCache<Issue[]>({
26
+ ttl: LINEAR_CACHE_TTL,
27
+ maxSize: 10,
28
+ })
29
+
30
+ /**
31
+ * Cache for teams list
32
+ * Key format: "teams"
33
+ */
34
+ export const teamsCache = new TTLCache<Array<{ id: string; name: string; key?: string }>>({
35
+ ttl: LINEAR_CACHE_TTL,
36
+ maxSize: 5,
37
+ })
38
+
39
+ /**
40
+ * Cache for projects list
41
+ * Key format: "projects"
42
+ */
43
+ export const projectsCache = new TTLCache<Array<{ id: string; name: string }>>({
44
+ ttl: LINEAR_CACHE_TTL,
45
+ maxSize: 5,
46
+ })
47
+
48
+ /**
49
+ * Clear all Linear caches
50
+ */
51
+ export function clearLinearCache(): void {
52
+ issueCache.clear()
53
+ assignedIssuesCache.clear()
54
+ teamsCache.clear()
55
+ projectsCache.clear()
56
+ }
57
+
58
+ /**
59
+ * Get cache statistics for debugging
60
+ */
61
+ export function getLinearCacheStats() {
62
+ return {
63
+ issues: issueCache.stats(),
64
+ assignedIssues: assignedIssuesCache.stats(),
65
+ teams: teamsCache.stats(),
66
+ projects: projectsCache.stats(),
67
+ }
68
+ }
@@ -1,6 +1,20 @@
1
1
  /**
2
2
  * Linear Integration
3
- * Issue tracker provider for Linear.
3
+ * Issue tracker provider for Linear using @linear/sdk
4
4
  */
5
5
 
6
+ // Core provider
6
7
  export { LinearProvider, linearProvider } from './client'
8
+
9
+ // Service layer with caching (preferred API)
10
+ export { LinearService, linearService } from './service'
11
+
12
+ // Cache utilities
13
+ export {
14
+ issueCache,
15
+ assignedIssuesCache,
16
+ teamsCache,
17
+ projectsCache,
18
+ clearLinearCache,
19
+ getLinearCacheStats,
20
+ } from './cache'
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Linear Service Layer
3
+ * Wraps LinearProvider with caching for improved performance.
4
+ * All operations are cached with 5-minute TTL.
5
+ */
6
+
7
+ import { linearProvider } from './client'
8
+ import {
9
+ issueCache,
10
+ assignedIssuesCache,
11
+ teamsCache,
12
+ projectsCache,
13
+ clearLinearCache,
14
+ getLinearCacheStats,
15
+ } from './cache'
16
+ import type {
17
+ Issue,
18
+ CreateIssueInput,
19
+ UpdateIssueInput,
20
+ FetchOptions,
21
+ LinearConfig,
22
+ } from '../issue-tracker/types'
23
+
24
+ export class LinearService {
25
+ private initialized = false
26
+ private userId: string | null = null
27
+
28
+ /**
29
+ * Check if service is ready
30
+ */
31
+ isReady(): boolean {
32
+ return this.initialized && linearProvider.isConfigured()
33
+ }
34
+
35
+ /**
36
+ * Initialize the service with config
37
+ * Must be called before any operations
38
+ */
39
+ async initialize(config: LinearConfig): Promise<void> {
40
+ if (this.initialized) return
41
+
42
+ await linearProvider.initialize(config)
43
+ this.initialized = true
44
+ }
45
+
46
+ /**
47
+ * Initialize from API key directly
48
+ * Convenience method for simple setup
49
+ */
50
+ async initializeFromApiKey(apiKey: string, teamId?: string): Promise<void> {
51
+ const config: LinearConfig = {
52
+ enabled: true,
53
+ provider: 'linear',
54
+ apiKey,
55
+ defaultTeamId: teamId,
56
+ syncOn: { task: true, done: true, ship: true },
57
+ enrichment: { enabled: true, updateProvider: true },
58
+ }
59
+ await this.initialize(config)
60
+ }
61
+
62
+ /**
63
+ * Get issues assigned to current user (cached)
64
+ */
65
+ async fetchAssignedIssues(options?: FetchOptions): Promise<Issue[]> {
66
+ this.ensureInitialized()
67
+
68
+ const cacheKey = `assigned:${this.userId || 'me'}`
69
+ const cached = assignedIssuesCache.get(cacheKey)
70
+ if (cached) {
71
+ return cached
72
+ }
73
+
74
+ const issues = await linearProvider.fetchAssignedIssues(options)
75
+ assignedIssuesCache.set(cacheKey, issues)
76
+
77
+ // Also cache individual issues
78
+ for (const issue of issues) {
79
+ issueCache.set(`issue:${issue.id}`, issue)
80
+ issueCache.set(`issue:${issue.externalId}`, issue)
81
+ }
82
+
83
+ return issues
84
+ }
85
+
86
+ /**
87
+ * Get issues from a team (cached)
88
+ */
89
+ async fetchTeamIssues(teamId: string, options?: FetchOptions): Promise<Issue[]> {
90
+ this.ensureInitialized()
91
+
92
+ const cacheKey = `team:${teamId}`
93
+ const cached = assignedIssuesCache.get(cacheKey)
94
+ if (cached) {
95
+ return cached
96
+ }
97
+
98
+ const issues = await linearProvider.fetchTeamIssues(teamId, options)
99
+ assignedIssuesCache.set(cacheKey, issues)
100
+
101
+ // Also cache individual issues
102
+ for (const issue of issues) {
103
+ issueCache.set(`issue:${issue.id}`, issue)
104
+ issueCache.set(`issue:${issue.externalId}`, issue)
105
+ }
106
+
107
+ return issues
108
+ }
109
+
110
+ /**
111
+ * Get a single issue by ID or identifier (cached)
112
+ * Accepts UUID or identifier like "PRJ-123"
113
+ */
114
+ async fetchIssue(id: string): Promise<Issue | null> {
115
+ this.ensureInitialized()
116
+
117
+ // Check cache first
118
+ const cacheKey = `issue:${id}`
119
+ const cached = issueCache.get(cacheKey)
120
+ if (cached) {
121
+ return cached
122
+ }
123
+
124
+ const issue = await linearProvider.fetchIssue(id)
125
+ if (issue) {
126
+ // Cache by both ID and externalId
127
+ issueCache.set(`issue:${issue.id}`, issue)
128
+ issueCache.set(`issue:${issue.externalId}`, issue)
129
+ }
130
+
131
+ return issue
132
+ }
133
+
134
+ /**
135
+ * Create a new issue (invalidates assigned cache)
136
+ */
137
+ async createIssue(input: CreateIssueInput): Promise<Issue> {
138
+ this.ensureInitialized()
139
+
140
+ const issue = await linearProvider.createIssue(input)
141
+
142
+ // Cache the new issue
143
+ issueCache.set(`issue:${issue.id}`, issue)
144
+ issueCache.set(`issue:${issue.externalId}`, issue)
145
+
146
+ // Invalidate assigned issues cache (new issue may be assigned)
147
+ assignedIssuesCache.clear()
148
+
149
+ return issue
150
+ }
151
+
152
+ /**
153
+ * Update an issue (invalidates cache for that issue)
154
+ */
155
+ async updateIssue(id: string, input: UpdateIssueInput): Promise<Issue> {
156
+ this.ensureInitialized()
157
+
158
+ const issue = await linearProvider.updateIssue(id, input)
159
+
160
+ // Update cache
161
+ issueCache.set(`issue:${issue.id}`, issue)
162
+ issueCache.set(`issue:${issue.externalId}`, issue)
163
+
164
+ return issue
165
+ }
166
+
167
+ /**
168
+ * Mark issue as in progress (invalidates cache)
169
+ */
170
+ async markInProgress(id: string): Promise<void> {
171
+ this.ensureInitialized()
172
+
173
+ await linearProvider.markInProgress(id)
174
+
175
+ // Invalidate caches
176
+ issueCache.delete(`issue:${id}`)
177
+ assignedIssuesCache.clear()
178
+ }
179
+
180
+ /**
181
+ * Mark issue as done (invalidates cache)
182
+ */
183
+ async markDone(id: string): Promise<void> {
184
+ this.ensureInitialized()
185
+
186
+ await linearProvider.markDone(id)
187
+
188
+ // Invalidate caches
189
+ issueCache.delete(`issue:${id}`)
190
+ assignedIssuesCache.clear()
191
+ }
192
+
193
+ /**
194
+ * Add a comment to an issue
195
+ */
196
+ async addComment(id: string, body: string): Promise<void> {
197
+ this.ensureInitialized()
198
+ await linearProvider.addComment(id, body)
199
+ }
200
+
201
+ /**
202
+ * Get available teams (cached)
203
+ */
204
+ async getTeams(): Promise<Array<{ id: string; name: string; key?: string }>> {
205
+ this.ensureInitialized()
206
+
207
+ const cached = teamsCache.get('teams')
208
+ if (cached) {
209
+ return cached
210
+ }
211
+
212
+ const teams = await linearProvider.getTeams()
213
+ teamsCache.set('teams', teams)
214
+ return teams
215
+ }
216
+
217
+ /**
218
+ * Get available projects (cached)
219
+ */
220
+ async getProjects(): Promise<Array<{ id: string; name: string }>> {
221
+ this.ensureInitialized()
222
+
223
+ const cached = projectsCache.get('projects')
224
+ if (cached) {
225
+ return cached
226
+ }
227
+
228
+ const projects = await linearProvider.getProjects()
229
+ projectsCache.set('projects', projects)
230
+ return projects
231
+ }
232
+
233
+ /**
234
+ * Clear all caches
235
+ */
236
+ clearCache(): void {
237
+ clearLinearCache()
238
+ }
239
+
240
+ /**
241
+ * Get cache statistics for debugging
242
+ */
243
+ getCacheStats() {
244
+ return getLinearCacheStats()
245
+ }
246
+
247
+ /**
248
+ * Ensure service is initialized
249
+ */
250
+ private ensureInitialized(): void {
251
+ if (!this.initialized) {
252
+ throw new Error(
253
+ 'Linear service not initialized. Call linearService.initialize() first or run `p. linear setup`.'
254
+ )
255
+ }
256
+ }
257
+ }
258
+
259
+ // Singleton instance
260
+ export const linearService = new LinearService()