prjct-cli 0.40.0 → 0.42.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.42.0] - 2026-01-29
4
+
5
+ ### Feature: Linear SDK Integration with Per-Project Credentials
6
+
7
+ Linear integration now uses the native `@linear/sdk` with per-project credential storage, enabling different projects to use different Linear workspaces.
8
+
9
+ **New: Per-Project Credentials**
10
+ - Credentials stored at `~/.prjct-cli/projects/{projectId}/config/credentials.json`
11
+ - Fallback chain: project credentials → macOS keychain → environment variable
12
+ - Each project can connect to a different Linear workspace
13
+
14
+ **New: CLI Bridge (`core/cli/linear.ts`)**
15
+ - Direct access to Linear SDK from templates
16
+ - Commands: `setup`, `list`, `get`, `create`, `update`, `start`, `done`, `comment`, `teams`, `projects`, `status`
17
+ - JSON output for easy parsing by Claude
18
+
19
+ **New: `prjct linear <cmd>` Subcommand**
20
+ - Direct CLI access: `prjct linear status`, `prjct linear list`, etc.
21
+ - Auto-resolves project ID from current directory
22
+
23
+ **Updated Templates**
24
+ - `linear.md` — Natural language interpretation guide for Claude
25
+ - `enrich.md` — Uses CLI instead of MCP for ticket enrichment
26
+
27
+ **Breaking: Removed MCP-only Trackers**
28
+ - Removed JIRA, GitHub Issues, Monday.com support (no native SDK)
29
+ - Only Linear is supported (has native SDK)
30
+
31
+ **New Files:**
32
+ - `core/cli/linear.ts` — CLI bridge for Linear SDK
33
+ - `core/utils/project-credentials.ts` — Per-project credential storage
34
+
35
+ **Modified:**
36
+ - `bin/prjct.ts` — Added `prjct linear` subcommand
37
+ - `templates/commands/linear.md` — Updated with CLI execution pattern
38
+ - `templates/commands/enrich.md` — Linear-only, uses CLI
39
+
3
40
  ## [0.40.0] - 2026-01-28
4
41
 
5
42
  ### Feature: Enhanced Skill System
package/bin/prjct.ts CHANGED
@@ -85,6 +85,32 @@ if (args[0] === 'start' || args[0] === 'setup') {
85
85
  console.error('Server error:', (error as Error).message)
86
86
  process.exitCode = 1
87
87
  }
88
+ } else if (args[0] === 'linear') {
89
+ // Linear CLI subcommand - direct access to Linear SDK
90
+ const { spawn } = await import('child_process')
91
+ const projectPath = process.cwd()
92
+ const projectId = await configManager.getProjectId(projectPath)
93
+
94
+ if (!projectId) {
95
+ console.error('No prjct project found. Run "prjct init" first.')
96
+ process.exitCode = 1
97
+ } else {
98
+ // Get the path to the linear CLI
99
+ const linearCliPath = path.join(__dirname, '..', 'core', 'cli', 'linear.ts')
100
+
101
+ // Forward args to linear CLI, adding --project flag
102
+ const linearArgs = ['--project', projectId, ...args.slice(1)]
103
+
104
+ // Use bun to run the CLI
105
+ const child = spawn('bun', [linearCliPath, ...linearArgs], {
106
+ stdio: 'inherit',
107
+ cwd: projectPath,
108
+ })
109
+
110
+ child.on('close', (code) => {
111
+ process.exitCode = code || 0
112
+ })
113
+ }
88
114
  } else if (args[0] === 'version' || args[0] === '-v' || args[0] === '--version') {
89
115
  // Show version with provider status
90
116
  const detection = detectAllProviders()
@@ -0,0 +1,381 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Linear CLI - Bridge between templates and SDK
4
+ *
5
+ * Usage: bun core/cli/linear.ts --project <projectId> <command> [args...]
6
+ *
7
+ * Commands:
8
+ * setup <apiKey> [teamId] - Store API key in project credentials
9
+ * list - List my assigned issues
10
+ * list-team <teamId> - List issues from a team
11
+ * get <id> - Get issue by ID or identifier (PRJ-123)
12
+ * create <json> - Create issue from JSON input
13
+ * update <id> <json> - Update issue
14
+ * start <id> - Mark issue as in progress
15
+ * done <id> - Mark issue as done
16
+ * comment <id> <text> - Add comment to issue
17
+ * teams - List available teams
18
+ * projects - List available projects
19
+ * status - Check connection status
20
+ *
21
+ * All output is JSON for easy parsing by Claude.
22
+ */
23
+
24
+ import { linearService } from '../integrations/linear'
25
+ import {
26
+ getLinearApiKey,
27
+ setLinearCredentials,
28
+ getProjectCredentials,
29
+ getCredentialSource,
30
+ } from '../utils/project-credentials'
31
+
32
+ // Parse arguments
33
+ const args = process.argv.slice(2)
34
+
35
+ // Extract --project flag
36
+ const projectIdx = args.indexOf('--project')
37
+ let projectId: string | null = null
38
+ if (projectIdx !== -1 && args[projectIdx + 1]) {
39
+ projectId = args[projectIdx + 1]
40
+ args.splice(projectIdx, 2)
41
+ }
42
+
43
+ const [command, ...commandArgs] = args
44
+
45
+ /**
46
+ * Output result as JSON
47
+ */
48
+ function output(data: unknown): void {
49
+ console.log(JSON.stringify(data, null, 2))
50
+ }
51
+
52
+ /**
53
+ * Output error as JSON and exit
54
+ */
55
+ function error(message: string, code = 1): never {
56
+ console.error(JSON.stringify({ error: message }))
57
+ process.exit(code)
58
+ }
59
+
60
+ /**
61
+ * Initialize Linear service from project credentials
62
+ */
63
+ async function initFromProject(): Promise<void> {
64
+ if (!projectId) {
65
+ error('No --project specified. Usage: linear.ts --project <projectId> <command>')
66
+ }
67
+
68
+ const apiKey = await getLinearApiKey(projectId)
69
+ if (!apiKey) {
70
+ error('Linear not configured. Run: p. linear setup')
71
+ }
72
+
73
+ const creds = await getProjectCredentials(projectId)
74
+ await linearService.initializeFromApiKey(apiKey, creds.linear?.teamId)
75
+ }
76
+
77
+ /**
78
+ * Main CLI handler
79
+ */
80
+ async function main(): Promise<void> {
81
+ try {
82
+ switch (command) {
83
+ case 'setup': {
84
+ if (!projectId) {
85
+ error('--project required for setup')
86
+ }
87
+
88
+ const apiKey = commandArgs[0]
89
+ const teamId = commandArgs[1] // optional
90
+
91
+ if (!apiKey) {
92
+ error('API key required. Usage: setup <apiKey> [teamId]')
93
+ }
94
+
95
+ // Test connection first
96
+ await linearService.initializeFromApiKey(apiKey, teamId)
97
+ const teams = await linearService.getTeams()
98
+
99
+ if (teams.length === 0) {
100
+ error('No teams found. Check your API key permissions.')
101
+ }
102
+
103
+ // Determine default team
104
+ let selectedTeamId = teamId
105
+ let selectedTeamKey: string | undefined
106
+
107
+ if (!selectedTeamId && teams.length === 1) {
108
+ selectedTeamId = teams[0].id
109
+ selectedTeamKey = teams[0].key
110
+ } else if (selectedTeamId) {
111
+ const team = teams.find((t) => t.id === selectedTeamId || t.key === selectedTeamId)
112
+ if (team) {
113
+ selectedTeamId = team.id
114
+ selectedTeamKey = team.key
115
+ }
116
+ }
117
+
118
+ // Store in project credentials
119
+ await setLinearCredentials(projectId, {
120
+ apiKey,
121
+ teamId: selectedTeamId,
122
+ teamKey: selectedTeamKey,
123
+ setupAt: new Date().toISOString(),
124
+ })
125
+
126
+ output({
127
+ success: true,
128
+ teams,
129
+ defaultTeam: selectedTeamId
130
+ ? { id: selectedTeamId, key: selectedTeamKey }
131
+ : null,
132
+ })
133
+ break
134
+ }
135
+
136
+ case 'list': {
137
+ await initFromProject()
138
+ const limit = commandArgs[0] ? parseInt(commandArgs[0], 10) : 20
139
+ const issues = await linearService.fetchAssignedIssues({ limit })
140
+ output({
141
+ count: issues.length,
142
+ issues: issues.map((issue) => ({
143
+ id: issue.id,
144
+ identifier: issue.externalId,
145
+ title: issue.title,
146
+ status: issue.status,
147
+ priority: issue.priority,
148
+ url: issue.url,
149
+ })),
150
+ })
151
+ break
152
+ }
153
+
154
+ case 'list-team': {
155
+ await initFromProject()
156
+ const teamId = commandArgs[0]
157
+ const limit = commandArgs[1] ? parseInt(commandArgs[1], 10) : 20
158
+
159
+ if (!teamId) {
160
+ error('Team ID required. Usage: list-team <teamId> [limit]')
161
+ }
162
+
163
+ const issues = await linearService.fetchTeamIssues(teamId, { limit })
164
+ output({
165
+ count: issues.length,
166
+ issues: issues.map((issue) => ({
167
+ id: issue.id,
168
+ identifier: issue.externalId,
169
+ title: issue.title,
170
+ status: issue.status,
171
+ priority: issue.priority,
172
+ url: issue.url,
173
+ })),
174
+ })
175
+ break
176
+ }
177
+
178
+ case 'get': {
179
+ await initFromProject()
180
+ const id = commandArgs[0]
181
+
182
+ if (!id) {
183
+ error('Issue ID required. Usage: get <id>')
184
+ }
185
+
186
+ const issue = await linearService.fetchIssue(id)
187
+ if (!issue) {
188
+ error(`Issue not found: ${id}`)
189
+ }
190
+
191
+ output(issue)
192
+ break
193
+ }
194
+
195
+ case 'create': {
196
+ await initFromProject()
197
+ const inputJson = commandArgs[0]
198
+
199
+ if (!inputJson) {
200
+ error('JSON input required. Usage: create \'{"title":"...", "teamId":"..."}\'')
201
+ }
202
+
203
+ let input
204
+ try {
205
+ input = JSON.parse(inputJson)
206
+ } catch {
207
+ error(`Invalid JSON: ${inputJson}`)
208
+ }
209
+
210
+ if (!input.title) {
211
+ error('title is required')
212
+ }
213
+ if (!input.teamId) {
214
+ // Try to use default team from credentials
215
+ const creds = await getProjectCredentials(projectId!)
216
+ if (creds.linear?.teamId) {
217
+ input.teamId = creds.linear.teamId
218
+ } else {
219
+ error('teamId is required (no default team configured)')
220
+ }
221
+ }
222
+
223
+ const issue = await linearService.createIssue(input)
224
+ output(issue)
225
+ break
226
+ }
227
+
228
+ case 'update': {
229
+ await initFromProject()
230
+ const id = commandArgs[0]
231
+ const inputJson = commandArgs[1]
232
+
233
+ if (!id) {
234
+ error('Issue ID required. Usage: update <id> \'{"description":"..."}\'')
235
+ }
236
+ if (!inputJson) {
237
+ error('JSON input required. Usage: update <id> \'{"description":"..."}\'')
238
+ }
239
+
240
+ let input
241
+ try {
242
+ input = JSON.parse(inputJson)
243
+ } catch {
244
+ error(`Invalid JSON: ${inputJson}`)
245
+ }
246
+
247
+ const issue = await linearService.updateIssue(id, input)
248
+ output(issue)
249
+ break
250
+ }
251
+
252
+ case 'start': {
253
+ await initFromProject()
254
+ const id = commandArgs[0]
255
+
256
+ if (!id) {
257
+ error('Issue ID required. Usage: start <id>')
258
+ }
259
+
260
+ await linearService.markInProgress(id)
261
+ output({ success: true, id, status: 'in_progress' })
262
+ break
263
+ }
264
+
265
+ case 'done': {
266
+ await initFromProject()
267
+ const id = commandArgs[0]
268
+
269
+ if (!id) {
270
+ error('Issue ID required. Usage: done <id>')
271
+ }
272
+
273
+ await linearService.markDone(id)
274
+ output({ success: true, id, status: 'done' })
275
+ break
276
+ }
277
+
278
+ case 'comment': {
279
+ await initFromProject()
280
+ const id = commandArgs[0]
281
+ const body = commandArgs.slice(1).join(' ')
282
+
283
+ if (!id) {
284
+ error('Issue ID required. Usage: comment <id> <text>')
285
+ }
286
+ if (!body) {
287
+ error('Comment text required. Usage: comment <id> <text>')
288
+ }
289
+
290
+ await linearService.addComment(id, body)
291
+ output({ success: true, id })
292
+ break
293
+ }
294
+
295
+ case 'teams': {
296
+ await initFromProject()
297
+ const teams = await linearService.getTeams()
298
+ output({ count: teams.length, teams })
299
+ break
300
+ }
301
+
302
+ case 'projects': {
303
+ await initFromProject()
304
+ const projects = await linearService.getProjects()
305
+ output({ count: projects.length, projects })
306
+ break
307
+ }
308
+
309
+ case 'status': {
310
+ if (!projectId) {
311
+ error('--project required for status')
312
+ }
313
+
314
+ const source = await getCredentialSource(projectId)
315
+ const apiKey = await getLinearApiKey(projectId)
316
+ const creds = await getProjectCredentials(projectId)
317
+
318
+ if (!apiKey) {
319
+ output({
320
+ configured: false,
321
+ source: 'none',
322
+ message: 'Linear not configured. Run: p. linear setup',
323
+ })
324
+ break
325
+ }
326
+
327
+ // Test connection
328
+ try {
329
+ await linearService.initializeFromApiKey(apiKey, creds.linear?.teamId)
330
+ const teams = await linearService.getTeams()
331
+
332
+ output({
333
+ configured: true,
334
+ source,
335
+ teamId: creds.linear?.teamId,
336
+ teamKey: creds.linear?.teamKey,
337
+ teamsAvailable: teams.length,
338
+ })
339
+ } catch (err) {
340
+ output({
341
+ configured: true,
342
+ source,
343
+ connectionError: (err as Error).message,
344
+ })
345
+ }
346
+ break
347
+ }
348
+
349
+ case 'help':
350
+ case '--help':
351
+ case '-h':
352
+ case undefined: {
353
+ output({
354
+ usage: 'linear.ts --project <projectId> <command> [args...]',
355
+ commands: {
356
+ setup: 'setup <apiKey> [teamId] - Store API key',
357
+ list: 'list [limit] - List my assigned issues',
358
+ 'list-team': 'list-team <teamId> [limit] - List team issues',
359
+ get: 'get <id> - Get issue by ID or identifier',
360
+ create: 'create <json> - Create issue',
361
+ update: 'update <id> <json> - Update issue',
362
+ start: 'start <id> - Mark as in progress',
363
+ done: 'done <id> - Mark as done',
364
+ comment: 'comment <id> <text> - Add comment',
365
+ teams: 'teams - List available teams',
366
+ projects: 'projects - List available projects',
367
+ status: 'status - Check connection',
368
+ },
369
+ })
370
+ break
371
+ }
372
+
373
+ default:
374
+ error(`Unknown command: ${command}. Use --help to see available commands.`)
375
+ }
376
+ } catch (err) {
377
+ error((err as Error).message)
378
+ }
379
+ }
380
+
381
+ main()
@@ -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,