prjct-cli 0.41.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,148 @@
1
+ /**
2
+ * Per-project credential storage
3
+ *
4
+ * Stores credentials in: ~/.prjct-cli/projects/{projectId}/config/credentials.json
5
+ *
6
+ * Fallback chain for Linear API key:
7
+ * 1. Project credentials (per-project)
8
+ * 2. Global keychain (macOS)
9
+ * 3. Environment variables
10
+ *
11
+ * This allows different projects to use different Linear workspaces.
12
+ */
13
+
14
+ import fs from 'fs'
15
+ import path from 'path'
16
+ import os from 'os'
17
+ import { getCredential, type CredentialKey } from './keychain'
18
+
19
+ interface LinearCredentials {
20
+ apiKey: string
21
+ teamId?: string
22
+ teamKey?: string
23
+ setupAt: string
24
+ }
25
+
26
+ export interface ProjectCredentials {
27
+ linear?: LinearCredentials
28
+ }
29
+
30
+ /**
31
+ * Get path to project credentials file
32
+ */
33
+ function getCredentialsPath(projectId: string): string {
34
+ return path.join(
35
+ os.homedir(),
36
+ '.prjct-cli',
37
+ 'projects',
38
+ projectId,
39
+ 'config',
40
+ 'credentials.json'
41
+ )
42
+ }
43
+
44
+ /**
45
+ * Get all credentials for a project
46
+ */
47
+ export async function getProjectCredentials(projectId: string): Promise<ProjectCredentials> {
48
+ const credPath = getCredentialsPath(projectId)
49
+ if (!fs.existsSync(credPath)) {
50
+ return {}
51
+ }
52
+
53
+ try {
54
+ return JSON.parse(fs.readFileSync(credPath, 'utf-8'))
55
+ } catch (error) {
56
+ console.error('[project-credentials] Failed to read credentials:', (error as Error).message)
57
+ return {}
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Set Linear credentials for a project
63
+ */
64
+ export async function setLinearCredentials(
65
+ projectId: string,
66
+ credentials: LinearCredentials
67
+ ): Promise<void> {
68
+ const credPath = getCredentialsPath(projectId)
69
+ const dir = path.dirname(credPath)
70
+
71
+ // Ensure directory exists
72
+ if (!fs.existsSync(dir)) {
73
+ fs.mkdirSync(dir, { recursive: true })
74
+ }
75
+
76
+ // Read existing, merge, write
77
+ const current = await getProjectCredentials(projectId)
78
+ current.linear = credentials
79
+ fs.writeFileSync(credPath, JSON.stringify(current, null, 2))
80
+ }
81
+
82
+ /**
83
+ * Delete Linear credentials for a project
84
+ */
85
+ export async function deleteLinearCredentials(projectId: string): Promise<void> {
86
+ const current = await getProjectCredentials(projectId)
87
+
88
+ if (current.linear) {
89
+ delete current.linear
90
+ const credPath = getCredentialsPath(projectId)
91
+ fs.writeFileSync(credPath, JSON.stringify(current, null, 2))
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Get Linear API key with fallback chain:
97
+ * 1. Project credentials
98
+ * 2. Global keychain
99
+ * 3. Environment variable
100
+ */
101
+ export async function getLinearApiKey(projectId: string): Promise<string | null> {
102
+ // 1. Project credentials
103
+ const projectCreds = await getProjectCredentials(projectId)
104
+ if (projectCreds.linear?.apiKey) {
105
+ return projectCreds.linear.apiKey
106
+ }
107
+
108
+ // 2. Global keychain (falls back to env var internally)
109
+ return getCredential('linear-api-key' as CredentialKey)
110
+ }
111
+
112
+ /**
113
+ * Get Linear team ID from project credentials
114
+ */
115
+ export async function getLinearTeamId(projectId: string): Promise<string | undefined> {
116
+ const projectCreds = await getProjectCredentials(projectId)
117
+ return projectCreds.linear?.teamId
118
+ }
119
+
120
+ /**
121
+ * Check if Linear is configured for a project
122
+ */
123
+ export async function isLinearConfigured(projectId: string): Promise<boolean> {
124
+ const apiKey = await getLinearApiKey(projectId)
125
+ return apiKey !== null && apiKey.length > 0
126
+ }
127
+
128
+ /**
129
+ * Get credential source for debugging
130
+ */
131
+ export async function getCredentialSource(
132
+ projectId: string
133
+ ): Promise<'project' | 'keychain' | 'env' | 'none'> {
134
+ // Check project credentials
135
+ const projectCreds = await getProjectCredentials(projectId)
136
+ if (projectCreds.linear?.apiKey) {
137
+ return 'project'
138
+ }
139
+
140
+ // Check keychain/env
141
+ const { getCredentialWithSource } = await import('./keychain')
142
+ const result = await getCredentialWithSource('linear-api-key' as CredentialKey)
143
+ if (result.value) {
144
+ return result.source === 'keychain' ? 'keychain' : 'env'
145
+ }
146
+
147
+ return 'none'
148
+ }
@@ -15888,7 +15888,7 @@ var require_package = __commonJS({
15888
15888
  "package.json"(exports, module) {
15889
15889
  module.exports = {
15890
15890
  name: "prjct-cli",
15891
- version: "0.40.0",
15891
+ version: "0.41.0",
15892
15892
  description: "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
15893
15893
  main: "core/index.ts",
15894
15894
  bin: {
@@ -17033,6 +17033,24 @@ if (args[0] === "start" || args[0] === "setup") {
17033
17033
  console.error("Server error:", error.message);
17034
17034
  process.exitCode = 1;
17035
17035
  }
17036
+ } else if (args[0] === "linear") {
17037
+ const { spawn } = await import("child_process");
17038
+ const projectPath = process.cwd();
17039
+ const projectId = await config_manager_default.getProjectId(projectPath);
17040
+ if (!projectId) {
17041
+ console.error('No prjct project found. Run "prjct init" first.');
17042
+ process.exitCode = 1;
17043
+ } else {
17044
+ const linearCliPath = path40.join(__dirname, "..", "core", "cli", "linear.ts");
17045
+ const linearArgs = ["--project", projectId, ...args.slice(1)];
17046
+ const child = spawn("bun", [linearCliPath, ...linearArgs], {
17047
+ stdio: "inherit",
17048
+ cwd: projectPath
17049
+ });
17050
+ child.on("close", (code) => {
17051
+ process.exitCode = code || 0;
17052
+ });
17053
+ }
17036
17054
  } else if (args[0] === "version" || args[0] === "-v" || args[0] === "--version") {
17037
17055
  const detection = detectAllProviders();
17038
17056
  const home = os13.homedir();