prjct-cli 0.41.0 → 0.44.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 +115 -0
- package/bin/prjct.ts +26 -0
- package/core/agentic/command-executor.ts +15 -5
- package/core/ai-tools/formatters.ts +302 -0
- package/core/ai-tools/generator.ts +124 -0
- package/core/ai-tools/index.ts +15 -0
- package/core/ai-tools/registry.ts +195 -0
- package/core/cli/linear.ts +440 -0
- package/core/commands/analysis.ts +36 -2
- package/core/commands/commands.ts +2 -2
- package/core/commands/planning.ts +8 -4
- package/core/commands/shipping.ts +8 -6
- package/core/commands/workflow.ts +67 -17
- package/core/index.ts +3 -1
- package/core/integrations/issue-tracker/types.ts +7 -1
- package/core/integrations/linear/client.ts +56 -24
- package/core/integrations/linear/index.ts +3 -0
- package/core/integrations/linear/sync.ts +313 -0
- package/core/schemas/index.ts +3 -0
- package/core/schemas/issues.ts +144 -0
- package/core/schemas/state.ts +3 -0
- package/core/services/sync-service.ts +71 -4
- package/core/utils/agent-stream.ts +138 -0
- package/core/utils/next-steps.ts +95 -0
- package/core/utils/output.ts +26 -0
- package/core/utils/project-credentials.ts +148 -0
- package/core/workflow/index.ts +6 -0
- package/core/workflow/state-machine.ts +185 -0
- package/dist/bin/prjct.mjs +2399 -540
- package/dist/core/infrastructure/setup.js +238 -192
- package/package.json +1 -1
- package/templates/_bases/tracker-base.md +11 -0
- package/templates/commands/done.md +18 -13
- package/templates/commands/enrich.md +152 -18
- package/templates/commands/linear.md +169 -135
- package/templates/commands/sync.md +17 -0
- package/templates/commands/task.md +20 -11
- package/templates/global/CLAUDE.md +58 -0
|
@@ -154,8 +154,8 @@ class PrjctCommands {
|
|
|
154
154
|
return this.analysis.analyze(options, projectPath)
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
async sync(projectPath: string = process.cwd()): Promise<CommandResult> {
|
|
158
|
-
return this.analysis.sync(projectPath)
|
|
157
|
+
async sync(projectPath: string = process.cwd(), options: { aiTools?: string[] } = {}): Promise<CommandResult> {
|
|
158
|
+
return this.analysis.sync(projectPath, options)
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
// ========== Context Commands ==========
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { queueStorage, ideasStorage } from '../storage'
|
|
22
22
|
import authorDetector from '../infrastructure/author-detector'
|
|
23
23
|
import commandInstaller from '../infrastructure/command-installer'
|
|
24
|
+
import { showNextSteps } from '../utils/next-steps'
|
|
24
25
|
|
|
25
26
|
// Lazy-loaded to avoid circular dependencies
|
|
26
27
|
let _analysisCommands: import('./analysis').AnalysisCommands | null = null
|
|
@@ -47,7 +48,7 @@ export class PlanningCommands extends PrjctCommandsBase {
|
|
|
47
48
|
return { success: false, message: 'Already initialized' }
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
out.
|
|
51
|
+
out.step(1, 4, 'Detecting author...')
|
|
51
52
|
|
|
52
53
|
const detectedAuthor = await authorDetector.detect()
|
|
53
54
|
// Convert null to undefined for createConfig
|
|
@@ -59,7 +60,7 @@ export class PlanningCommands extends PrjctCommandsBase {
|
|
|
59
60
|
const config = await configManager.createConfig(projectPath, author)
|
|
60
61
|
const projectId = config.projectId
|
|
61
62
|
|
|
62
|
-
out.
|
|
63
|
+
out.step(2, 4, 'Creating structure...')
|
|
63
64
|
|
|
64
65
|
await pathManager.ensureProjectStructure(projectId)
|
|
65
66
|
const globalPath = pathManager.getGlobalProjectPath(projectId)
|
|
@@ -91,12 +92,12 @@ export class PlanningCommands extends PrjctCommandsBase {
|
|
|
91
92
|
const hasCode = await this._detectExistingCode(projectPath)
|
|
92
93
|
|
|
93
94
|
if (hasCode || !isEmpty) {
|
|
94
|
-
out.
|
|
95
|
+
out.step(3, 4, 'Analyzing project...')
|
|
95
96
|
const analysis = await getAnalysisCommands()
|
|
96
97
|
const analysisResult = await analysis.analyze({}, projectPath)
|
|
97
98
|
|
|
98
99
|
if (analysisResult.success) {
|
|
99
|
-
out.
|
|
100
|
+
out.step(4, 4, 'Generating agents...')
|
|
100
101
|
await analysis.sync(projectPath)
|
|
101
102
|
out.done('initialized')
|
|
102
103
|
return { success: true, mode: 'existing', projectId }
|
|
@@ -123,6 +124,7 @@ export class PlanningCommands extends PrjctCommandsBase {
|
|
|
123
124
|
await commandInstaller.installGlobalConfig()
|
|
124
125
|
|
|
125
126
|
out.done('initialized')
|
|
127
|
+
showNextSteps('init')
|
|
126
128
|
return { success: true, projectId }
|
|
127
129
|
} catch (error) {
|
|
128
130
|
out.fail((error as Error).message)
|
|
@@ -250,6 +252,7 @@ export class PlanningCommands extends PrjctCommandsBase {
|
|
|
250
252
|
})
|
|
251
253
|
|
|
252
254
|
out.done(`bug [${severity}] → ${agent}`)
|
|
255
|
+
showNextSteps('bug')
|
|
253
256
|
|
|
254
257
|
return { success: true, bug: description, severity, agent }
|
|
255
258
|
} catch (error) {
|
|
@@ -416,6 +419,7 @@ Generated: ${new Date().toLocaleString()}
|
|
|
416
419
|
})
|
|
417
420
|
|
|
418
421
|
out.done(`idea captured: ${description.slice(0, 40)}`)
|
|
422
|
+
showNextSteps('idea')
|
|
419
423
|
|
|
420
424
|
return { success: true, mode: 'capture', idea: description }
|
|
421
425
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
out
|
|
19
19
|
} from './base'
|
|
20
20
|
import { stateStorage, shippedStorage } from '../storage'
|
|
21
|
+
import { showNextSteps } from '../utils/next-steps'
|
|
21
22
|
|
|
22
23
|
export class ShippingCommands extends PrjctCommandsBase {
|
|
23
24
|
/**
|
|
@@ -70,22 +71,22 @@ export class ShippingCommands extends PrjctCommandsBase {
|
|
|
70
71
|
featureName = currentTask?.description || 'current work'
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
// Ship steps with progress indicator
|
|
75
|
+
out.step(1, 5, `Linting ${featureName}...`)
|
|
75
76
|
const lintResult = await this._runLint(projectPath)
|
|
76
77
|
|
|
77
|
-
out.
|
|
78
|
+
out.step(2, 5, 'Running tests...')
|
|
78
79
|
const testResult = await this._runTests(projectPath)
|
|
79
80
|
|
|
80
|
-
out.
|
|
81
|
+
out.step(3, 5, 'Updating version...')
|
|
81
82
|
const newVersion = await this._bumpVersion(projectPath)
|
|
82
83
|
await this._updateChangelog(featureName, newVersion, projectPath)
|
|
83
84
|
|
|
84
|
-
out.
|
|
85
|
+
out.step(4, 5, 'Committing...')
|
|
85
86
|
const commitResult = await this._createShipCommit(featureName, projectPath)
|
|
86
87
|
|
|
87
88
|
if (commitResult.success) {
|
|
88
|
-
out.
|
|
89
|
+
out.step(5, 5, 'Pushing...')
|
|
89
90
|
await this._gitPush(projectPath)
|
|
90
91
|
}
|
|
91
92
|
|
|
@@ -116,6 +117,7 @@ export class ShippingCommands extends PrjctCommandsBase {
|
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
out.done(`v${newVersion} shipped`)
|
|
120
|
+
showNextSteps('ship')
|
|
119
121
|
|
|
120
122
|
return { success: true, feature: featureName, version: newVersion }
|
|
121
123
|
} catch (error) {
|
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
import { stateStorage, queueStorage } from '../storage'
|
|
21
21
|
import { templateExecutor } from '../agentic/template-executor'
|
|
22
22
|
import commandExecutor from '../agentic/command-executor'
|
|
23
|
+
import { showNextSteps, showStateInfo } from '../utils/next-steps'
|
|
24
|
+
import { workflowStateMachine } from '../workflow/state-machine'
|
|
25
|
+
import { linearService } from '../integrations/linear'
|
|
26
|
+
import { getProjectCredentials, getLinearApiKey } from '../utils/project-credentials'
|
|
23
27
|
|
|
24
28
|
export class WorkflowCommands extends PrjctCommandsBase {
|
|
25
29
|
/**
|
|
@@ -45,24 +49,39 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
45
49
|
return { success: false, error: result.error }
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
// Check if task is a Linear issue ID (e.g., PRJ-139)
|
|
53
|
+
let linearId: string | undefined
|
|
54
|
+
let taskDescription = task
|
|
55
|
+
const linearPattern = /^[A-Z]+-\d+$/
|
|
56
|
+
if (linearPattern.test(task)) {
|
|
57
|
+
try {
|
|
58
|
+
const creds = await getProjectCredentials(projectId)
|
|
59
|
+
const apiKey = await getLinearApiKey(projectId)
|
|
60
|
+
if (apiKey && creds.linear?.teamId) {
|
|
61
|
+
await linearService.initializeFromApiKey(
|
|
62
|
+
apiKey,
|
|
63
|
+
creds.linear.teamId
|
|
64
|
+
)
|
|
65
|
+
const issue = await linearService.fetchIssue(task)
|
|
66
|
+
if (issue) {
|
|
67
|
+
linearId = task
|
|
68
|
+
taskDescription = `${task}: ${issue.title}`
|
|
69
|
+
// Mark as in progress in Linear
|
|
70
|
+
await linearService.markInProgress(task)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
// Linear fetch failed - continue with task as-is
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
48
78
|
// Write-through: JSON → MD → Event
|
|
49
79
|
await stateStorage.startTask(projectId, {
|
|
50
80
|
id: generateUUID(),
|
|
51
|
-
description:
|
|
52
|
-
sessionId: generateUUID()
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
// Log orchestrator results if available
|
|
56
|
-
if (result.orchestratorContext) {
|
|
57
|
-
const oc = result.orchestratorContext
|
|
58
|
-
const agentsList = oc.agents.map((a: { name: string }) => a.name).join(', ') || 'none'
|
|
59
|
-
const domainsList = oc.detectedDomains.join(', ')
|
|
60
|
-
console.log(`🎯 Orchestrator: ${domainsList} → [${agentsList}]`)
|
|
61
|
-
|
|
62
|
-
if (oc.requiresFragmentation && oc.subtasks) {
|
|
63
|
-
console.log(` → ${oc.subtasks.length} subtasks created`)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
81
|
+
description: taskDescription,
|
|
82
|
+
sessionId: generateUUID(),
|
|
83
|
+
linearId,
|
|
84
|
+
} as Parameters<typeof stateStorage.startTask>[1])
|
|
66
85
|
|
|
67
86
|
// Get available agents for backward compatibility
|
|
68
87
|
const availableAgents = await templateExecutor.getAvailableAgents(projectPath)
|
|
@@ -70,7 +89,9 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
70
89
|
? availableAgents.join(', ')
|
|
71
90
|
: 'none (run p. sync)'
|
|
72
91
|
|
|
73
|
-
out.done(`${task}
|
|
92
|
+
out.done(`${task}`)
|
|
93
|
+
showStateInfo('working')
|
|
94
|
+
showNextSteps('task')
|
|
74
95
|
|
|
75
96
|
await this.logToMemory(projectPath, 'task_started', {
|
|
76
97
|
task,
|
|
@@ -139,7 +160,31 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
139
160
|
// Write-through: Complete task (JSON → MD → Event)
|
|
140
161
|
await stateStorage.completeTask(projectId)
|
|
141
162
|
|
|
142
|
-
|
|
163
|
+
// Sync to Linear if task has linearId
|
|
164
|
+
const linearId = (currentTask as { linearId?: string }).linearId
|
|
165
|
+
if (linearId) {
|
|
166
|
+
try {
|
|
167
|
+
const creds = await getProjectCredentials(projectId)
|
|
168
|
+
const apiKey = await getLinearApiKey(projectId)
|
|
169
|
+
if (apiKey && creds.linear?.teamId) {
|
|
170
|
+
await linearService.initializeFromApiKey(
|
|
171
|
+
apiKey,
|
|
172
|
+
creds.linear.teamId
|
|
173
|
+
)
|
|
174
|
+
await linearService.markDone(linearId)
|
|
175
|
+
out.done(`${task}${duration ? ` (${duration})` : ''} → Linear ✓`)
|
|
176
|
+
} else {
|
|
177
|
+
out.done(`${task}${duration ? ` (${duration})` : ''}`)
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// Linear sync failed silently - don't block the workflow
|
|
181
|
+
out.done(`${task}${duration ? ` (${duration})` : ''}`)
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
out.done(`${task}${duration ? ` (${duration})` : ''}`)
|
|
185
|
+
}
|
|
186
|
+
showStateInfo('completed')
|
|
187
|
+
showNextSteps('done')
|
|
143
188
|
|
|
144
189
|
await this.logToMemory(projectPath, 'task_completed', {
|
|
145
190
|
task,
|
|
@@ -176,6 +221,7 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
176
221
|
}
|
|
177
222
|
|
|
178
223
|
out.done(`${tasks.length} task${tasks.length !== 1 ? 's' : ''} queued`)
|
|
224
|
+
showNextSteps('next')
|
|
179
225
|
|
|
180
226
|
return { success: true, tasks, count: tasks.length }
|
|
181
227
|
} catch (error) {
|
|
@@ -210,6 +256,8 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
210
256
|
|
|
211
257
|
const taskDesc = currentTask.description.slice(0, 40)
|
|
212
258
|
out.done(`paused: ${taskDesc}${reason ? ` (${reason})` : ''}`)
|
|
259
|
+
showStateInfo('paused')
|
|
260
|
+
showNextSteps('pause')
|
|
213
261
|
|
|
214
262
|
await this.logToMemory(projectPath, 'task_paused', {
|
|
215
263
|
task: currentTask.description,
|
|
@@ -254,6 +302,8 @@ export class WorkflowCommands extends PrjctCommandsBase {
|
|
|
254
302
|
}
|
|
255
303
|
|
|
256
304
|
out.done(`resumed: ${resumed.description.slice(0, 40)}`)
|
|
305
|
+
showStateInfo('working')
|
|
306
|
+
showNextSteps('resume')
|
|
257
307
|
|
|
258
308
|
await this.logToMemory(projectPath, 'task_resumed', {
|
|
259
309
|
task: resumed.description,
|
package/core/index.ts
CHANGED
|
@@ -118,7 +118,9 @@ async function main(): Promise<void> {
|
|
|
118
118
|
redo: () => commands.redo(),
|
|
119
119
|
history: () => commands.history(),
|
|
120
120
|
// Setup
|
|
121
|
-
sync: () => commands.sync(),
|
|
121
|
+
sync: () => commands.sync(process.cwd(), {
|
|
122
|
+
aiTools: options.agents ? String(options.agents).split(',') : undefined,
|
|
123
|
+
}),
|
|
122
124
|
start: () => commands.start(),
|
|
123
125
|
// Context (for Claude templates)
|
|
124
126
|
context: (p) => commands.context(p),
|
|
@@ -75,10 +75,16 @@ export interface CreateIssueInput {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
|
-
* Update input for
|
|
78
|
+
* Update input for issues
|
|
79
79
|
*/
|
|
80
80
|
export interface UpdateIssueInput {
|
|
81
|
+
title?: string
|
|
81
82
|
description?: string
|
|
83
|
+
priority?: IssuePriority
|
|
84
|
+
assigneeId?: string | null // null to unassign
|
|
85
|
+
stateId?: string
|
|
86
|
+
projectId?: string
|
|
87
|
+
labels?: string[]
|
|
82
88
|
// Provider-specific: may update custom fields for AC, etc.
|
|
83
89
|
customFields?: Record<string, unknown>
|
|
84
90
|
}
|
|
@@ -84,7 +84,8 @@ export class LinearProvider implements IssueTrackerProvider {
|
|
|
84
84
|
// Verify connection
|
|
85
85
|
try {
|
|
86
86
|
const viewer = await this.sdk.viewer
|
|
87
|
-
|
|
87
|
+
// Use stderr for logs to not break JSON output
|
|
88
|
+
console.error(`[linear] Connected as ${viewer.name} (${viewer.email})`)
|
|
88
89
|
} catch (error) {
|
|
89
90
|
this.sdk = null
|
|
90
91
|
throw new Error(`Linear connection failed: ${(error as Error).message}`)
|
|
@@ -93,16 +94,28 @@ export class LinearProvider implements IssueTrackerProvider {
|
|
|
93
94
|
|
|
94
95
|
/**
|
|
95
96
|
* Get issues assigned to current user
|
|
97
|
+
* Filters by configured team if defaultTeamId is set
|
|
96
98
|
*/
|
|
97
99
|
async fetchAssignedIssues(options?: FetchOptions): Promise<Issue[]> {
|
|
98
100
|
if (!this.sdk) throw new Error('Linear not initialized')
|
|
99
101
|
|
|
100
102
|
const viewer = await this.sdk.viewer
|
|
103
|
+
|
|
104
|
+
// Build filter - always filter by team if configured
|
|
105
|
+
const filter: Record<string, unknown> = {}
|
|
106
|
+
|
|
107
|
+
if (!options?.includeCompleted) {
|
|
108
|
+
filter.state = { type: { nin: ['completed', 'canceled'] } }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Filter by configured team to only show relevant issues
|
|
112
|
+
if (this.config?.defaultTeamId) {
|
|
113
|
+
filter.team = { id: { eq: this.config.defaultTeamId } }
|
|
114
|
+
}
|
|
115
|
+
|
|
101
116
|
const assignedIssues = await viewer.assignedIssues({
|
|
102
117
|
first: options?.limit || 50,
|
|
103
|
-
filter:
|
|
104
|
-
? undefined
|
|
105
|
-
: { state: { type: { nin: ['completed', 'canceled'] } } },
|
|
118
|
+
filter: Object.keys(filter).length > 0 ? filter : undefined,
|
|
106
119
|
})
|
|
107
120
|
|
|
108
121
|
return Promise.all(
|
|
@@ -128,28 +141,34 @@ export class LinearProvider implements IssueTrackerProvider {
|
|
|
128
141
|
}
|
|
129
142
|
|
|
130
143
|
/**
|
|
131
|
-
* Get a single issue by ID or identifier (e.g., "
|
|
144
|
+
* Get a single issue by ID or identifier (e.g., "PRJ-123")
|
|
132
145
|
*/
|
|
133
146
|
async fetchIssue(id: string): Promise<Issue | null> {
|
|
134
147
|
if (!this.sdk) throw new Error('Linear not initialized')
|
|
135
148
|
|
|
136
149
|
try {
|
|
137
|
-
// Check if it looks like an identifier (e.g., "
|
|
150
|
+
// Check if it looks like an identifier (e.g., "PRJ-123")
|
|
138
151
|
if (id.includes('-') && /^[A-Z]+-\d+$/.test(id)) {
|
|
139
|
-
//
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
// Parse identifier into team key and issue number
|
|
153
|
+
const match = id.match(/^([A-Z]+)-(\d+)$/)
|
|
154
|
+
if (!match) return null
|
|
155
|
+
|
|
156
|
+
const [, teamKey, numberStr] = match
|
|
157
|
+
const issueNumber = parseInt(numberStr, 10)
|
|
158
|
+
|
|
159
|
+
// Find team by key
|
|
160
|
+
const teams = await this.sdk.teams({ first: 50 })
|
|
161
|
+
const team = teams.nodes.find((t) => t.key === teamKey)
|
|
162
|
+
if (!team) return null
|
|
163
|
+
|
|
164
|
+
// Query issue by team and number
|
|
165
|
+
const issues = await team.issues({
|
|
166
|
+
first: 1,
|
|
167
|
+
filter: { number: { eq: issueNumber } },
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
if (issues.nodes.length > 0) {
|
|
171
|
+
return this.mapIssue(issues.nodes[0])
|
|
153
172
|
}
|
|
154
173
|
return null
|
|
155
174
|
}
|
|
@@ -197,15 +216,28 @@ export class LinearProvider implements IssueTrackerProvider {
|
|
|
197
216
|
async updateIssue(id: string, input: UpdateIssueInput): Promise<Issue> {
|
|
198
217
|
if (!this.sdk) throw new Error('Linear not initialized')
|
|
199
218
|
|
|
200
|
-
// Get the issue first to get UUID
|
|
219
|
+
// Get the issue first to get UUID (if identifier like PRJ-123 was passed)
|
|
201
220
|
const issue = await this.fetchIssue(id)
|
|
202
221
|
if (!issue) {
|
|
203
222
|
throw new Error(`Issue ${id} not found`)
|
|
204
223
|
}
|
|
205
224
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
225
|
+
// Build update payload with all supported fields
|
|
226
|
+
const updatePayload: Record<string, unknown> = {}
|
|
227
|
+
|
|
228
|
+
if (input.title !== undefined) updatePayload.title = input.title
|
|
229
|
+
if (input.description !== undefined) updatePayload.description = input.description
|
|
230
|
+
if (input.priority !== undefined) updatePayload.priority = PRIORITY_TO_LINEAR[input.priority]
|
|
231
|
+
if (input.assigneeId !== undefined) updatePayload.assigneeId = input.assigneeId
|
|
232
|
+
if (input.stateId !== undefined) updatePayload.stateId = input.stateId
|
|
233
|
+
if (input.projectId !== undefined) updatePayload.projectId = input.projectId
|
|
234
|
+
|
|
235
|
+
// Handle labels - need to resolve names to IDs
|
|
236
|
+
if (input.labels !== undefined && issue.team) {
|
|
237
|
+
updatePayload.labelIds = await this.resolveLabelIds(issue.team.id, input.labels)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await this.sdk.updateIssue(issue.id, updatePayload)
|
|
209
241
|
|
|
210
242
|
// Fetch updated issue
|
|
211
243
|
const updated = await this.fetchIssue(issue.id)
|
|
@@ -9,6 +9,9 @@ export { LinearProvider, linearProvider } from './client'
|
|
|
9
9
|
// Service layer with caching (preferred API)
|
|
10
10
|
export { LinearService, linearService } from './service'
|
|
11
11
|
|
|
12
|
+
// Sync layer for bidirectional sync with issues.json
|
|
13
|
+
export { LinearSync, linearSync } from './sync'
|
|
14
|
+
|
|
12
15
|
// Cache utilities
|
|
13
16
|
export {
|
|
14
17
|
issueCache,
|