@dcyfr/ai 3.0.1 → 3.0.3
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 +10 -0
- package/LICENSE +11 -30
- package/README.md +216 -145
- package/dist/.tsbuildinfo +1 -1
- package/dist/ai/agents/agent-router.d.ts.map +1 -1
- package/dist/ai/agents/agent-router.js +3 -1
- package/dist/ai/agents/agent-router.js.map +1 -1
- package/dist/ai/config/loader.d.ts +5 -1
- package/dist/ai/config/loader.d.ts.map +1 -1
- package/dist/ai/config/loader.js +24 -4
- package/dist/ai/config/loader.js.map +1 -1
- package/dist/ai/core/provider-registry.d.ts +9 -1
- package/dist/ai/core/provider-registry.d.ts.map +1 -1
- package/dist/ai/core/provider-registry.js +97 -138
- package/dist/ai/core/provider-registry.js.map +1 -1
- package/dist/ai/core/telemetry-engine.d.ts +1 -1
- package/dist/ai/core/telemetry-engine.d.ts.map +1 -1
- package/dist/ai/core/telemetry-engine.js +14 -10
- package/dist/ai/core/telemetry-engine.js.map +1 -1
- package/dist/ai/delegation/contract-manager.d.ts.map +1 -1
- package/dist/ai/delegation/contract-manager.js +5 -2
- package/dist/ai/delegation/contract-manager.js.map +1 -1
- package/dist/ai/delegation/execution-mode-dashboard.d.ts.map +1 -1
- package/dist/ai/delegation/execution-mode-dashboard.js +4 -2
- package/dist/ai/delegation/execution-mode-dashboard.js.map +1 -1
- package/dist/ai/mcp/servers/shared/utils.d.ts.map +1 -1
- package/dist/ai/mcp/servers/shared/utils.js +4 -2
- package/dist/ai/mcp/servers/shared/utils.js.map +1 -1
- package/dist/ai/memory/file-memory-adapter.d.ts.map +1 -1
- package/dist/ai/memory/file-memory-adapter.js +11 -13
- package/dist/ai/memory/file-memory-adapter.js.map +1 -1
- package/dist/ai/memory/working-memory-persistence.d.ts.map +1 -1
- package/dist/ai/memory/working-memory-persistence.js +4 -2
- package/dist/ai/memory/working-memory-persistence.js.map +1 -1
- package/dist/ai/metacognition/config.d.ts +41 -0
- package/dist/ai/metacognition/config.d.ts.map +1 -0
- package/dist/ai/metacognition/config.js +51 -0
- package/dist/ai/metacognition/config.js.map +1 -0
- package/dist/ai/metacognition/governance.d.ts +68 -0
- package/dist/ai/metacognition/governance.d.ts.map +1 -0
- package/dist/ai/metacognition/governance.js +118 -0
- package/dist/ai/metacognition/governance.js.map +1 -0
- package/dist/ai/metacognition/index.d.ts +24 -0
- package/dist/ai/metacognition/index.d.ts.map +1 -0
- package/dist/ai/metacognition/index.js +18 -0
- package/dist/ai/metacognition/index.js.map +1 -0
- package/dist/ai/metacognition/ledger.d.ts +121 -0
- package/dist/ai/metacognition/ledger.d.ts.map +1 -0
- package/dist/ai/metacognition/ledger.js +268 -0
- package/dist/ai/metacognition/ledger.js.map +1 -0
- package/dist/ai/metacognition/runtime.d.ts +205 -0
- package/dist/ai/metacognition/runtime.d.ts.map +1 -0
- package/dist/ai/metacognition/runtime.js +391 -0
- package/dist/ai/metacognition/runtime.js.map +1 -0
- package/dist/ai/metacognition/telemetry.d.ts +144 -0
- package/dist/ai/metacognition/telemetry.d.ts.map +1 -0
- package/dist/ai/metacognition/telemetry.js +149 -0
- package/dist/ai/metacognition/telemetry.js.map +1 -0
- package/dist/ai/metacognition/transfer.d.ts +153 -0
- package/dist/ai/metacognition/transfer.d.ts.map +1 -0
- package/dist/ai/metacognition/transfer.js +182 -0
- package/dist/ai/metacognition/transfer.js.map +1 -0
- package/dist/ai/metacognition/types.d.ts +302 -0
- package/dist/ai/metacognition/types.d.ts.map +1 -0
- package/dist/ai/metacognition/types.js +79 -0
- package/dist/ai/metacognition/types.js.map +1 -0
- package/dist/ai/runtime/agent-runtime.d.ts.map +1 -1
- package/dist/ai/runtime/agent-runtime.js +12 -18
- package/dist/ai/runtime/agent-runtime.js.map +1 -1
- package/dist/ai/src/capability-manifest-generator.d.ts.map +1 -1
- package/dist/ai/src/capability-manifest-generator.js +11 -5
- package/dist/ai/src/capability-manifest-generator.js.map +1 -1
- package/dist/ai/src/cli/telemetry-dashboard.js +3 -3
- package/dist/ai/src/cli/telemetry-dashboard.js.map +1 -1
- package/dist/ai/src/compaction/memory-compaction.d.ts.map +1 -1
- package/dist/ai/src/compaction/memory-compaction.js +5 -4
- package/dist/ai/src/compaction/memory-compaction.js.map +1 -1
- package/dist/ai/src/integrations/linear/index.d.ts +19 -0
- package/dist/ai/src/integrations/linear/index.d.ts.map +1 -0
- package/dist/ai/src/integrations/linear/index.js +20 -0
- package/dist/ai/src/integrations/linear/index.js.map +1 -0
- package/dist/ai/src/integrations/linear/issue-mapper.d.ts +93 -0
- package/dist/ai/src/integrations/linear/issue-mapper.d.ts.map +1 -0
- package/dist/ai/src/integrations/linear/issue-mapper.js +186 -0
- package/dist/ai/src/integrations/linear/issue-mapper.js.map +1 -0
- package/dist/ai/src/integrations/linear/linear-client.d.ts +199 -0
- package/dist/ai/src/integrations/linear/linear-client.d.ts.map +1 -0
- package/dist/ai/src/integrations/linear/linear-client.js +300 -0
- package/dist/ai/src/integrations/linear/linear-client.js.map +1 -0
- package/dist/ai/src/plugins/security/secret-detector.js +3 -3
- package/dist/ai/src/plugins/security/secret-detector.js.map +1 -1
- package/dist/ai/src/runtime/agent-runtime.js +1 -1
- package/dist/ai/src/runtime/agent-runtime.js.map +1 -1
- package/dist/ai/src/security/prompt-scan-worker.d.ts +63 -0
- package/dist/ai/src/security/prompt-scan-worker.d.ts.map +1 -0
- package/dist/ai/src/security/prompt-scan-worker.js +177 -0
- package/dist/ai/src/security/prompt-scan-worker.js.map +1 -0
- package/dist/ai/src/telemetry/delegation-telemetry.d.ts +10 -0
- package/dist/ai/src/telemetry/delegation-telemetry.d.ts.map +1 -1
- package/dist/ai/src/telemetry/delegation-telemetry.js +23 -0
- package/dist/ai/src/telemetry/delegation-telemetry.js.map +1 -1
- package/dist/ai/types/index.d.ts +8 -3
- package/dist/ai/types/index.d.ts.map +1 -1
- package/dist/ai/types/index.js.map +1 -1
- package/dist/ai/utils/safe-fs.d.ts +19 -0
- package/dist/ai/utils/safe-fs.d.ts.map +1 -0
- package/dist/ai/utils/safe-fs.js +64 -0
- package/dist/ai/utils/safe-fs.js.map +1 -0
- package/package.json +33 -22
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue Key Correlation and Mapping
|
|
3
|
+
*
|
|
4
|
+
* Extracts issue keys from GitHub PR titles, branch names, and commit messages
|
|
5
|
+
* to automatically correlate them with Linear issues. Implements a priority-based
|
|
6
|
+
* matching strategy and stores mappings for future reference.
|
|
7
|
+
*
|
|
8
|
+
* Correlation Strategy (in order of confidence):
|
|
9
|
+
* 1. Branch name: `feature/DCYFR-123-description` [confidence: 0.95]
|
|
10
|
+
* 2. PR title: `[DCYFR-123] Feature description` [confidence: 0.85]
|
|
11
|
+
* 3. Commit message: Usually at end: `... Fixes DCYFR-123` [confidence: 0.75]
|
|
12
|
+
* 4. Manual mapping: User explicitly linked (stored in DB) [confidence: 1.0]
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* const mapper = new IssueMapper({ store: myMappingStore });
|
|
16
|
+
* const match = await mapper.correlate({
|
|
17
|
+
* branch: 'feature/DCYFR-123-auth',
|
|
18
|
+
* prTitle: '[DCYFR-123] Add OAuth',
|
|
19
|
+
* commits: ['Add auth module (#123)', 'Fixes DCYFR-123']
|
|
20
|
+
* });
|
|
21
|
+
* // => { linearIssueId: 'uuid-xxx', identifier: 'DCYFR-123', confidence: 0.95 }
|
|
22
|
+
*
|
|
23
|
+
* Part of: Phase 2 (MVP Linear Sync) + Phase 3 (Bidirectional Sync)
|
|
24
|
+
* Test: __tests__/integrations/linear/issue-mapper.test.ts
|
|
25
|
+
*/
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Issue Key Regex Patterns
|
|
28
|
+
// ============================================================================
|
|
29
|
+
/**
|
|
30
|
+
* Matches Jira-style issue keys: TEAM-NUMBER
|
|
31
|
+
* Examples: DCYFR-123, FOO-999
|
|
32
|
+
*
|
|
33
|
+
* Pattern explanation:
|
|
34
|
+
* [A-Z]+ = one or more uppercase letters (team key)
|
|
35
|
+
* - = hyphen
|
|
36
|
+
* \d+ = one or more digits (issue number)
|
|
37
|
+
*/
|
|
38
|
+
const ISSUE_KEY_PATTERN = /([A-Z][A-Z0-9]*-\d+)/g;
|
|
39
|
+
/**
|
|
40
|
+
* Branch name format: feature/TEAM-NUMBER-description
|
|
41
|
+
*/
|
|
42
|
+
const BRANCH_PATTERN = /^[a-z]+\/([A-Z][A-Z0-9]*-\d+)/;
|
|
43
|
+
/**
|
|
44
|
+
* PR title format: "[TEAM-NUMBER] Description" or "[TEAM-NUMBER] Description (Fixes)"
|
|
45
|
+
*/
|
|
46
|
+
const PR_TITLE_PATTERN = /^\[([A-Z][A-Z0-9]*-\d+)\]/;
|
|
47
|
+
/**
|
|
48
|
+
* Commit pattern: "Description (Fixes|Closes|Resolves) TEAM-NUMBER"
|
|
49
|
+
*/
|
|
50
|
+
const COMMIT_PATTERN = /(Fixes|Closes|Resolves|Fixed|Closed|Resolved)\s+([A-Z][A-Z0-9]*-\d+)/i;
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// IssueMapper
|
|
53
|
+
// ============================================================================
|
|
54
|
+
export class IssueMapper {
|
|
55
|
+
/**
|
|
56
|
+
* Extract issue key from a string, returning up to `limit` matches.
|
|
57
|
+
*/
|
|
58
|
+
static extractIssueKeys(text, limit = 5) {
|
|
59
|
+
const matches = text.matchAll(ISSUE_KEY_PATTERN);
|
|
60
|
+
return Array.from(matches)
|
|
61
|
+
.map(m => m[1])
|
|
62
|
+
.slice(0, limit)
|
|
63
|
+
.filter((v, i, a) => a.indexOf(v) === i); // Deduplicate
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Extract issue key from branch name (highest confidence).
|
|
67
|
+
* Pattern: feature/DCYFR-123-description
|
|
68
|
+
*/
|
|
69
|
+
static extractFromBranch(branch) {
|
|
70
|
+
const match = branch.match(BRANCH_PATTERN);
|
|
71
|
+
return match ? match[1] : null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Extract issue key from PR title (high confidence).
|
|
75
|
+
* Patterns: "[DCYFR-123] Title", "[DCYFR-123]: Title"
|
|
76
|
+
*/
|
|
77
|
+
static extractFromPrTitle(title) {
|
|
78
|
+
const match = title.match(PR_TITLE_PATTERN);
|
|
79
|
+
return match ? match[1] : null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Extract issue key from commit messages (moderate confidence).
|
|
83
|
+
* Patterns: "Fixes DCYFR-123", "Closes DCYFR-123", "Resolves DCYFR-123"
|
|
84
|
+
*/
|
|
85
|
+
static extractFromCommits(commits) {
|
|
86
|
+
for (const commit of commits) {
|
|
87
|
+
const match = commit.match(COMMIT_PATTERN);
|
|
88
|
+
if (match) {
|
|
89
|
+
return match[2]; // Return the issue key
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Extract issue keys from PR body/description (low confidence).
|
|
96
|
+
* Uses generic issue key pattern matching.
|
|
97
|
+
*/
|
|
98
|
+
static extractFromPrBody(body) {
|
|
99
|
+
return this.extractIssueKeys(body, 1);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Correlate a pull request to a Linear issue by examining branch, title, commits, and body.
|
|
103
|
+
*
|
|
104
|
+
* Returns the best match with highest confidence, plus alternates for manual review.
|
|
105
|
+
* Implements priority-based matching:
|
|
106
|
+
* 1. Branch name (0.95) — most specific
|
|
107
|
+
* 2. PR title (0.85)
|
|
108
|
+
* 3. Commit messages (0.75)
|
|
109
|
+
* 4. PR body (0.60) — lowest confidence
|
|
110
|
+
*/
|
|
111
|
+
static correlate(input) {
|
|
112
|
+
const candidates = [];
|
|
113
|
+
// 1. Branch (highest priority)
|
|
114
|
+
if (input.branch) {
|
|
115
|
+
const key = this.extractFromBranch(input.branch);
|
|
116
|
+
if (key) {
|
|
117
|
+
candidates.push({
|
|
118
|
+
identifier: key,
|
|
119
|
+
source: 'branch',
|
|
120
|
+
confidence: 0.95,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// 2. PR title
|
|
125
|
+
if (input.prTitle) {
|
|
126
|
+
const key = this.extractFromPrTitle(input.prTitle);
|
|
127
|
+
if (key) {
|
|
128
|
+
candidates.push({
|
|
129
|
+
identifier: key,
|
|
130
|
+
source: 'pr_title',
|
|
131
|
+
confidence: 0.85,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// 3. Commit messages
|
|
136
|
+
if (input.commits && input.commits.length > 0) {
|
|
137
|
+
const key = this.extractFromCommits(input.commits);
|
|
138
|
+
if (key) {
|
|
139
|
+
candidates.push({
|
|
140
|
+
identifier: key,
|
|
141
|
+
source: 'commit_message',
|
|
142
|
+
confidence: 0.75,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// 4. PR body
|
|
147
|
+
if (input.prBody) {
|
|
148
|
+
const keys = this.extractFromPrBody(input.prBody);
|
|
149
|
+
for (const key of keys) {
|
|
150
|
+
candidates.push({
|
|
151
|
+
identifier: key,
|
|
152
|
+
source: 'pr_body',
|
|
153
|
+
confidence: 0.60,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Return best match (by confidence, then by source priority)
|
|
158
|
+
if (candidates.length === 0) {
|
|
159
|
+
return {
|
|
160
|
+
identifier: null,
|
|
161
|
+
source: null,
|
|
162
|
+
confidence: 0,
|
|
163
|
+
alternates: [],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// Sort by confidence descending
|
|
167
|
+
candidates.sort((a, b) => b.confidence - a.confidence);
|
|
168
|
+
const best = candidates[0];
|
|
169
|
+
const alternates = candidates.slice(1);
|
|
170
|
+
return {
|
|
171
|
+
identifier: best.identifier,
|
|
172
|
+
source: best.source,
|
|
173
|
+
confidence: best.confidence,
|
|
174
|
+
alternates,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Validate that a correlation result meets minimum confidence threshold.
|
|
179
|
+
* Default threshold: 0.8 (requires high confidence)
|
|
180
|
+
*/
|
|
181
|
+
static isConfident(result, threshold = 0.8) {
|
|
182
|
+
return result.confidence >= threshold && result.identifier !== null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
export default IssueMapper;
|
|
186
|
+
//# sourceMappingURL=issue-mapper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"issue-mapper.js","sourceRoot":"","sources":["../../../../../packages/ai/src/integrations/linear/issue-mapper.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear API Client
|
|
3
|
+
*
|
|
4
|
+
* Provides a typed, authenticated wrapper around the Linear GraphQL API for
|
|
5
|
+
* reading/writing issues, creating comments, adding labels, and managing
|
|
6
|
+
* issue state transitions.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const client = new LinearClient({ apiKey: process.env.LINEAR_API_KEY });
|
|
10
|
+
* const issue = await client.getIssue('DCYFR-123');
|
|
11
|
+
* await client.addComment(issue.id, 'Review started');
|
|
12
|
+
* await client.updateIssueState(issue.id, 'In Review');
|
|
13
|
+
*
|
|
14
|
+
* Authentication:
|
|
15
|
+
* - Requires LINEAR_API_KEY environment variable (org API key)
|
|
16
|
+
* - API documentation: https://developers.linear.app/docs
|
|
17
|
+
* - GraphQL endpoint: https://api.linear.app/graphql
|
|
18
|
+
*
|
|
19
|
+
* Rate Limiting:
|
|
20
|
+
* - Linear API: 1000 requests/hour per token
|
|
21
|
+
* - Implements sliding-window rate limiter with exponential backoff
|
|
22
|
+
*
|
|
23
|
+
* Idempotency:
|
|
24
|
+
* - All write operations include deduplication keys
|
|
25
|
+
* - Prevents duplicate comments/updates on identical requests
|
|
26
|
+
*
|
|
27
|
+
* Error Handling:
|
|
28
|
+
* - GraphQL errors returned as structured `LinearError`
|
|
29
|
+
* - Network failures trigger exponential backoff retry
|
|
30
|
+
* - Rate limits surface as `RateLimitError`
|
|
31
|
+
*
|
|
32
|
+
* Part of: dcyfr-ai integration layer
|
|
33
|
+
* Roadmap: Phase 2 (MVP Linear Sync) + Phase 3 (Bidirectional Sync)
|
|
34
|
+
* Test: __tests__/integrations/linear/linear-client.test.ts
|
|
35
|
+
*/
|
|
36
|
+
type GraphQLError = {
|
|
37
|
+
message: string;
|
|
38
|
+
extensions?: Record<string, unknown>;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Linear API credentials and configuration.
|
|
42
|
+
*/
|
|
43
|
+
export interface LinearClientConfig {
|
|
44
|
+
/** Linear API key (org-scoped, from workspace settings) */
|
|
45
|
+
apiKey: string;
|
|
46
|
+
/** Optional: Override GraphQL endpoint (default: https://api.linear.app/graphql) */
|
|
47
|
+
graphqlEndpoint?: string;
|
|
48
|
+
/** Optional: Maximum retry attempts for transient failures (default: 3) */
|
|
49
|
+
maxRetries?: number;
|
|
50
|
+
/** Optional: Timeout in ms for API requests (default: 30000) */
|
|
51
|
+
requestTimeout?: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Mapping between Linear issue and GitHub PR/issue.
|
|
55
|
+
* Stored in persistent mapping store for correlation.
|
|
56
|
+
*/
|
|
57
|
+
export interface IssueMapping {
|
|
58
|
+
/** Linear issue ID (UUID) */
|
|
59
|
+
linearIssueId: string;
|
|
60
|
+
/** GitHub repository owner */
|
|
61
|
+
owner: string;
|
|
62
|
+
/** GitHub repository name */
|
|
63
|
+
repo: string;
|
|
64
|
+
/** GitHub PR number (if PR) or issue number (if issue) */
|
|
65
|
+
prOrIssueNumber: number;
|
|
66
|
+
/** How the correlation was established */
|
|
67
|
+
correlationSource: 'branch' | 'pr_title' | 'commit_message' | 'manual';
|
|
68
|
+
/** Confidence score (0-1); 1.0 = certain, < 0.8 = needs review */
|
|
69
|
+
confidence: number;
|
|
70
|
+
/** When the mapping was created */
|
|
71
|
+
createdAt: Date;
|
|
72
|
+
/** When the mapping was last verified */
|
|
73
|
+
lastVerifiedAt?: Date;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Linear issue with relevant fields for review/PR sync.
|
|
77
|
+
*/
|
|
78
|
+
export interface LinearIssue {
|
|
79
|
+
id: string;
|
|
80
|
+
identifier: string;
|
|
81
|
+
title: string;
|
|
82
|
+
description?: string;
|
|
83
|
+
state: {
|
|
84
|
+
id: string;
|
|
85
|
+
name: string;
|
|
86
|
+
};
|
|
87
|
+
assignee?: {
|
|
88
|
+
id: string;
|
|
89
|
+
name: string;
|
|
90
|
+
email: string;
|
|
91
|
+
};
|
|
92
|
+
team: {
|
|
93
|
+
id: string;
|
|
94
|
+
key: string;
|
|
95
|
+
};
|
|
96
|
+
labels: Array<{
|
|
97
|
+
id: string;
|
|
98
|
+
name: string;
|
|
99
|
+
}>;
|
|
100
|
+
createdAt: string;
|
|
101
|
+
updatedAt: string;
|
|
102
|
+
/** Custom field for linking to GitHub PR */
|
|
103
|
+
_githubPrUrl?: string;
|
|
104
|
+
/** Custom field for PR review status */
|
|
105
|
+
_prReviewStatus?: 'pending' | 'in_progress' | 'approved' | 'changes_requested';
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Linear issue state for workflow transitions.
|
|
109
|
+
*/
|
|
110
|
+
export interface LinearIssueState {
|
|
111
|
+
id: string;
|
|
112
|
+
name: string;
|
|
113
|
+
type: 'backlog' | 'unstarted' | 'started' | 'completed' | 'cancelled';
|
|
114
|
+
position: number;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Comment created on a Linear issue.
|
|
118
|
+
*/
|
|
119
|
+
export interface LinearComment {
|
|
120
|
+
id: string;
|
|
121
|
+
body: string;
|
|
122
|
+
author: {
|
|
123
|
+
id: string;
|
|
124
|
+
name: string;
|
|
125
|
+
};
|
|
126
|
+
createdAt: string;
|
|
127
|
+
updatedAt: string;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Linear API error with detailed context.
|
|
131
|
+
*/
|
|
132
|
+
export declare class LinearError extends Error {
|
|
133
|
+
readonly statusCode?: number | undefined;
|
|
134
|
+
readonly graphqlErrors?: GraphQLError[] | undefined;
|
|
135
|
+
constructor(message: string, statusCode?: number | undefined, graphqlErrors?: GraphQLError[] | undefined);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Rate limit error when API quota is exceeded.
|
|
139
|
+
*/
|
|
140
|
+
export declare class LinearRateLimitError extends LinearError {
|
|
141
|
+
readonly resetAt: Date;
|
|
142
|
+
constructor(message: string, resetAt: Date);
|
|
143
|
+
}
|
|
144
|
+
export declare class LinearClient {
|
|
145
|
+
private apiKey;
|
|
146
|
+
private graphqlEndpoint;
|
|
147
|
+
private maxRetries;
|
|
148
|
+
private requestTimeout;
|
|
149
|
+
private requestCount;
|
|
150
|
+
private requestResetAt;
|
|
151
|
+
constructor(config: LinearClientConfig);
|
|
152
|
+
/**
|
|
153
|
+
* Retrieve a Linear issue by identifier (e.g., "DCYFR-123").
|
|
154
|
+
*/
|
|
155
|
+
getIssue(identifier: string): Promise<LinearIssue>;
|
|
156
|
+
/**
|
|
157
|
+
* Get all available workflow states for a team.
|
|
158
|
+
* Used for determining valid state transitions.
|
|
159
|
+
*/
|
|
160
|
+
getWorkflowStates(teamId: string): Promise<LinearIssueState[]>;
|
|
161
|
+
/**
|
|
162
|
+
* Update an issue's state (e.g., "In Review" → "Changes Requested").
|
|
163
|
+
* Returns the updated issue.
|
|
164
|
+
*/
|
|
165
|
+
updateIssueState(issueId: string, newState: string): Promise<LinearIssue>;
|
|
166
|
+
/**
|
|
167
|
+
* Add a comment to a Linear issue.
|
|
168
|
+
* Includes deduplication key to prevent duplicate comments.
|
|
169
|
+
*/
|
|
170
|
+
addComment(issueId: string, body: string, deduplicationKey?: string): Promise<LinearComment>;
|
|
171
|
+
/**
|
|
172
|
+
* Add a label to a Linear issue.
|
|
173
|
+
*/
|
|
174
|
+
addLabel(issueId: string, labelId: string): Promise<void>;
|
|
175
|
+
/**
|
|
176
|
+
* Assign a Linear issue to a team member.
|
|
177
|
+
*/
|
|
178
|
+
assignIssue(issueId: string, userId: string): Promise<void>;
|
|
179
|
+
/**
|
|
180
|
+
* Search for issues by query (e.g., team or label filters).
|
|
181
|
+
* Used for finding issues linked to GitHub PRs.
|
|
182
|
+
*/
|
|
183
|
+
searchIssues(query: string, limit?: number): Promise<LinearIssue[]>;
|
|
184
|
+
/**
|
|
185
|
+
* Internal: Execute a GraphQL query/mutation with rate-limit and retry handling.
|
|
186
|
+
*/
|
|
187
|
+
private graphql;
|
|
188
|
+
/**
|
|
189
|
+
* Check if request is within rate limit quota.
|
|
190
|
+
* Resets hourly.
|
|
191
|
+
*/
|
|
192
|
+
private checkRateLimit;
|
|
193
|
+
/**
|
|
194
|
+
* Determine if an error is transient (can be retried).
|
|
195
|
+
*/
|
|
196
|
+
private isTransient;
|
|
197
|
+
}
|
|
198
|
+
export default LinearClient;
|
|
199
|
+
//# sourceMappingURL=linear-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linear-client.d.ts","sourceRoot":"","sources":["../../../../../packages/ai/src/integrations/linear/linear-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear API Client
|
|
3
|
+
*
|
|
4
|
+
* Provides a typed, authenticated wrapper around the Linear GraphQL API for
|
|
5
|
+
* reading/writing issues, creating comments, adding labels, and managing
|
|
6
|
+
* issue state transitions.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const client = new LinearClient({ apiKey: process.env.LINEAR_API_KEY });
|
|
10
|
+
* const issue = await client.getIssue('DCYFR-123');
|
|
11
|
+
* await client.addComment(issue.id, 'Review started');
|
|
12
|
+
* await client.updateIssueState(issue.id, 'In Review');
|
|
13
|
+
*
|
|
14
|
+
* Authentication:
|
|
15
|
+
* - Requires LINEAR_API_KEY environment variable (org API key)
|
|
16
|
+
* - API documentation: https://developers.linear.app/docs
|
|
17
|
+
* - GraphQL endpoint: https://api.linear.app/graphql
|
|
18
|
+
*
|
|
19
|
+
* Rate Limiting:
|
|
20
|
+
* - Linear API: 1000 requests/hour per token
|
|
21
|
+
* - Implements sliding-window rate limiter with exponential backoff
|
|
22
|
+
*
|
|
23
|
+
* Idempotency:
|
|
24
|
+
* - All write operations include deduplication keys
|
|
25
|
+
* - Prevents duplicate comments/updates on identical requests
|
|
26
|
+
*
|
|
27
|
+
* Error Handling:
|
|
28
|
+
* - GraphQL errors returned as structured `LinearError`
|
|
29
|
+
* - Network failures trigger exponential backoff retry
|
|
30
|
+
* - Rate limits surface as `RateLimitError`
|
|
31
|
+
*
|
|
32
|
+
* Part of: dcyfr-ai integration layer
|
|
33
|
+
* Roadmap: Phase 2 (MVP Linear Sync) + Phase 3 (Bidirectional Sync)
|
|
34
|
+
* Test: __tests__/integrations/linear/linear-client.test.ts
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Linear API error with detailed context.
|
|
38
|
+
*/
|
|
39
|
+
export class LinearError extends Error {
|
|
40
|
+
statusCode;
|
|
41
|
+
graphqlErrors;
|
|
42
|
+
constructor(message, statusCode, graphqlErrors) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.statusCode = statusCode;
|
|
45
|
+
this.graphqlErrors = graphqlErrors;
|
|
46
|
+
this.name = 'LinearError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Rate limit error when API quota is exceeded.
|
|
51
|
+
*/
|
|
52
|
+
export class LinearRateLimitError extends LinearError {
|
|
53
|
+
resetAt;
|
|
54
|
+
constructor(message, resetAt) {
|
|
55
|
+
super(message, 429);
|
|
56
|
+
this.resetAt = resetAt;
|
|
57
|
+
this.name = 'LinearRateLimitError';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Linear Client
|
|
62
|
+
// ============================================================================
|
|
63
|
+
export class LinearClient {
|
|
64
|
+
apiKey;
|
|
65
|
+
graphqlEndpoint;
|
|
66
|
+
maxRetries;
|
|
67
|
+
requestTimeout;
|
|
68
|
+
requestCount = 0;
|
|
69
|
+
requestResetAt = Date.now() + 3600000; // 1 hour from now
|
|
70
|
+
constructor(config) {
|
|
71
|
+
this.apiKey = config.apiKey;
|
|
72
|
+
this.graphqlEndpoint = config.graphqlEndpoint ?? 'https://api.linear.app/graphql';
|
|
73
|
+
this.maxRetries = config.maxRetries ?? 3;
|
|
74
|
+
this.requestTimeout = config.requestTimeout ?? 30000;
|
|
75
|
+
if (!this.apiKey) {
|
|
76
|
+
throw new LinearError('LINEAR_API_KEY is required');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Retrieve a Linear issue by identifier (e.g., "DCYFR-123").
|
|
81
|
+
*/
|
|
82
|
+
async getIssue(identifier) {
|
|
83
|
+
const query = `
|
|
84
|
+
query GetIssue($identifier: String!) {
|
|
85
|
+
issue(id: $identifier) {
|
|
86
|
+
id
|
|
87
|
+
identifier
|
|
88
|
+
title
|
|
89
|
+
description
|
|
90
|
+
state { id name }
|
|
91
|
+
assignee { id name email }
|
|
92
|
+
team { id key }
|
|
93
|
+
labels { id name }
|
|
94
|
+
createdAt
|
|
95
|
+
updatedAt
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
`;
|
|
99
|
+
const data = await this.graphql(query, { identifier });
|
|
100
|
+
return data.issue;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Get all available workflow states for a team.
|
|
104
|
+
* Used for determining valid state transitions.
|
|
105
|
+
*/
|
|
106
|
+
async getWorkflowStates(teamId) {
|
|
107
|
+
const query = `
|
|
108
|
+
query GetWorkflowStates($teamId: String!) {
|
|
109
|
+
team(id: $teamId) {
|
|
110
|
+
states {
|
|
111
|
+
nodes {
|
|
112
|
+
id
|
|
113
|
+
name
|
|
114
|
+
type
|
|
115
|
+
position
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
`;
|
|
121
|
+
const data = await this.graphql(query, { teamId });
|
|
122
|
+
return data.team.states.nodes;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Update an issue's state (e.g., "In Review" → "Changes Requested").
|
|
126
|
+
* Returns the updated issue.
|
|
127
|
+
*/
|
|
128
|
+
async updateIssueState(issueId, newState) {
|
|
129
|
+
const mutation = `
|
|
130
|
+
mutation UpdateIssueState($issueId: String!, $state: String!) {
|
|
131
|
+
issueUpdate(id: $issueId, input: { stateId: $state }) {
|
|
132
|
+
issue {
|
|
133
|
+
id
|
|
134
|
+
identifier
|
|
135
|
+
title
|
|
136
|
+
state { id name }
|
|
137
|
+
updatedAt
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
`;
|
|
142
|
+
const data = await this.graphql(mutation, { issueId, state: newState });
|
|
143
|
+
return data.issueUpdate.issue;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Add a comment to a Linear issue.
|
|
147
|
+
* Includes deduplication key to prevent duplicate comments.
|
|
148
|
+
*/
|
|
149
|
+
async addComment(issueId, body, deduplicationKey) {
|
|
150
|
+
const mutation = `
|
|
151
|
+
mutation AddComment($issueId: String!, $body: String!, $deduplicationKey: String) {
|
|
152
|
+
commentCreate(input: { issueId: $issueId, body: $body, deduplicationKey: $deduplicationKey }) {
|
|
153
|
+
comment {
|
|
154
|
+
id
|
|
155
|
+
body
|
|
156
|
+
author { id name }
|
|
157
|
+
createdAt
|
|
158
|
+
updatedAt
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
`;
|
|
163
|
+
const data = await this.graphql(mutation, { issueId, body, deduplicationKey });
|
|
164
|
+
return data.commentCreate.comment;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Add a label to a Linear issue.
|
|
168
|
+
*/
|
|
169
|
+
async addLabel(issueId, labelId) {
|
|
170
|
+
const mutation = `
|
|
171
|
+
mutation AddLabel($issueId: String!, $labelIds: [String!]!) {
|
|
172
|
+
issueUpdate(id: $issueId, input: { labelIds: $labelIds }) {
|
|
173
|
+
issue { id }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
`;
|
|
177
|
+
await this.graphql(mutation, { issueId, labelIds: [labelId] });
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Assign a Linear issue to a team member.
|
|
181
|
+
*/
|
|
182
|
+
async assignIssue(issueId, userId) {
|
|
183
|
+
const mutation = `
|
|
184
|
+
mutation AssignIssue($issueId: String!, $userId: String!) {
|
|
185
|
+
issueUpdate(id: $issueId, input: { assigneeId: $userId }) {
|
|
186
|
+
issue { id }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
`;
|
|
190
|
+
await this.graphql(mutation, { issueId, userId });
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Search for issues by query (e.g., team or label filters).
|
|
194
|
+
* Used for finding issues linked to GitHub PRs.
|
|
195
|
+
*/
|
|
196
|
+
async searchIssues(query, limit = 25) {
|
|
197
|
+
const graphqlQuery = `
|
|
198
|
+
query SearchIssues($query: String!, $first: Int!) {
|
|
199
|
+
issues(filter: $query, first: $first) {
|
|
200
|
+
nodes {
|
|
201
|
+
id
|
|
202
|
+
identifier
|
|
203
|
+
title
|
|
204
|
+
description
|
|
205
|
+
state { id name }
|
|
206
|
+
assignee { id name email }
|
|
207
|
+
team { id key }
|
|
208
|
+
labels { id name }
|
|
209
|
+
createdAt
|
|
210
|
+
updatedAt
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
`;
|
|
215
|
+
const data = await this.graphql(graphqlQuery, { query, first: limit });
|
|
216
|
+
return data.issues.nodes;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Internal: Execute a GraphQL query/mutation with rate-limit and retry handling.
|
|
220
|
+
*/
|
|
221
|
+
async graphql(query, variables, retryCount = 0) {
|
|
222
|
+
// Check rate limit
|
|
223
|
+
this.checkRateLimit();
|
|
224
|
+
const payload = {
|
|
225
|
+
query,
|
|
226
|
+
variables: variables ?? {},
|
|
227
|
+
};
|
|
228
|
+
const options = {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
headers: {
|
|
231
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
232
|
+
'Content-Type': 'application/json',
|
|
233
|
+
},
|
|
234
|
+
body: JSON.stringify(payload),
|
|
235
|
+
timeout: this.requestTimeout,
|
|
236
|
+
};
|
|
237
|
+
try {
|
|
238
|
+
const response = await fetch(this.graphqlEndpoint, options);
|
|
239
|
+
this.requestCount++;
|
|
240
|
+
// Handle rate limiting
|
|
241
|
+
if (response.status === 429) {
|
|
242
|
+
const resetAfter = response.headers.get('Retry-After');
|
|
243
|
+
const resetAt = new Date(Date.now() + (Number.parseInt(resetAfter ?? '60', 10) * 1000));
|
|
244
|
+
throw new LinearRateLimitError('Linear API rate limit exceeded', resetAt);
|
|
245
|
+
}
|
|
246
|
+
if (!response.ok) {
|
|
247
|
+
throw new LinearError(`Linear API returned ${response.status}`, response.status);
|
|
248
|
+
}
|
|
249
|
+
const data = await response.json();
|
|
250
|
+
// Handle GraphQL errors
|
|
251
|
+
if (data.errors && data.errors.length > 0) {
|
|
252
|
+
throw new LinearError(`GraphQL error: ${data.errors[0].message}`, 400, data.errors);
|
|
253
|
+
}
|
|
254
|
+
return data.data;
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
// Retry on transient errors
|
|
258
|
+
if (retryCount < this.maxRetries && this.isTransient(error)) {
|
|
259
|
+
const backoffMs = Math.pow(2, retryCount) * 1000;
|
|
260
|
+
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
|
261
|
+
return this.graphql(query, variables, retryCount + 1);
|
|
262
|
+
}
|
|
263
|
+
if (error instanceof LinearError) {
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
throw new LinearError(`Linear API request failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Check if request is within rate limit quota.
|
|
271
|
+
* Resets hourly.
|
|
272
|
+
*/
|
|
273
|
+
checkRateLimit() {
|
|
274
|
+
const now = Date.now();
|
|
275
|
+
// Reset counter if window has passed
|
|
276
|
+
if (now > this.requestResetAt) {
|
|
277
|
+
this.requestCount = 0;
|
|
278
|
+
this.requestResetAt = now + 3600000; // 1 hour from now
|
|
279
|
+
}
|
|
280
|
+
// Linear: 1000 requests/hour
|
|
281
|
+
if (this.requestCount >= 1000) {
|
|
282
|
+
throw new LinearRateLimitError('Linear API rate limit exceeded (1000/hour)', new Date(this.requestResetAt));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Determine if an error is transient (can be retried).
|
|
287
|
+
*/
|
|
288
|
+
isTransient(error) {
|
|
289
|
+
if (error instanceof LinearRateLimitError) {
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
if (error instanceof LinearError) {
|
|
293
|
+
// Only retry on server errors (5xx)
|
|
294
|
+
return error.statusCode ? error.statusCode >= 500 : false;
|
|
295
|
+
}
|
|
296
|
+
return true; // Assume network errors are transient
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
export default LinearClient;
|
|
300
|
+
//# sourceMappingURL=linear-client.js.map
|