prjct-cli 0.39.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.40.0] - 2026-01-28
4
+
5
+ ### Feature: Enhanced Skill System
6
+
7
+ Inspired by vercel-labs/skills, prjct-cli now supports remote skill installation, version tracking via lock file, and full ecosystem-standard SKILL.md subdirectory format across all skill directories.
8
+
9
+ **New: Remote Skill Installation (`p. skill add`)**
10
+ - Install skills from GitHub repos: `p. skill add owner/repo`
11
+ - Install specific skill: `p. skill add owner/repo@skill-name`
12
+ - Install from local directory: `p. skill add ./path`
13
+ - Automatic source metadata injection (`_prjct` frontmatter block)
14
+
15
+ **New: Skill Lock File**
16
+ - Tracks installed skills at `~/.prjct-cli/skills/.skill-lock.json`
17
+ - Records source URL, commit SHA, install timestamp
18
+ - Enables update detection via `p. skill check`
19
+
20
+ **New: Additional Subcommands**
21
+ - `p. skill add <source>` — Install from GitHub or local path
22
+ - `p. skill remove <name>` — Uninstall a skill
23
+ - `p. skill init <name>` — Scaffold a new skill template
24
+ - `p. skill check` — Detect available updates
25
+
26
+ **Improved: Full SKILL.md Subdirectory Support**
27
+ - All skill directories now support both `{name}.md` (flat) and `{name}/SKILL.md` (subdirectory) formats
28
+ - Previously only provider dirs (`~/.claude/skills/`) checked subdirectories
29
+ - Orchestrator executor now checks both patterns when loading agent skills
30
+
31
+ **New Files:**
32
+ - `core/services/skill-installer.ts` — Remote installation service
33
+ - `core/services/skill-lock.ts` — Lock file management
34
+
35
+ **Modified:**
36
+ - `core/types/services.ts` — Extended `SkillMetadata` with `sourceUrl`, `sourceType`, `installedAt`, `sha`; added `'remote'` source type
37
+ - `core/services/skill-service.ts` — Unified SKILL.md discovery in all dirs
38
+ - `core/agentic/orchestrator-executor.ts` — Both path patterns for skill loading
39
+ - `templates/commands/skill.md` — New subcommands documentation
40
+
3
41
  ## [0.39.0] - 2026-01-24
4
42
 
5
43
  ### Feature: Windsurf IDE Support (PRJ-66)
@@ -344,18 +344,35 @@ export class OrchestratorExecutor {
344
344
  // Skip if already loaded
345
345
  if (loadedSkillNames.has(skillName)) continue
346
346
 
347
- const skillPath = path.join(skillsDir, `${skillName}.md`)
347
+ // Check both patterns: flat file and subdirectory (ecosystem standard)
348
+ const flatPath = path.join(skillsDir, `${skillName}.md`)
349
+ const subdirPath = path.join(skillsDir, skillName, 'SKILL.md')
350
+
351
+ let content: string | null = null
352
+ let resolvedPath = flatPath
353
+
354
+ // Prefer subdirectory format (ecosystem standard)
348
355
  try {
349
- const content = await fs.readFile(skillPath, 'utf-8')
356
+ content = await fs.readFile(subdirPath, 'utf-8')
357
+ resolvedPath = subdirPath
358
+ } catch {
359
+ // Fall back to flat file
360
+ try {
361
+ content = await fs.readFile(flatPath, 'utf-8')
362
+ resolvedPath = flatPath
363
+ } catch {
364
+ // Skill not found - not an error, just skip
365
+ console.warn(`Skill not found: ${skillName}`)
366
+ }
367
+ }
368
+
369
+ if (content) {
350
370
  skills.push({
351
371
  name: skillName,
352
372
  content,
353
- filePath: skillPath,
373
+ filePath: resolvedPath,
354
374
  })
355
375
  loadedSkillNames.add(skillName)
356
- } catch {
357
- // Skill not found - not an error, just skip
358
- console.warn(`Skill not found: ${skillName}`)
359
376
  }
360
377
  }
361
378
  }
@@ -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'