@proletariat/cli 0.3.45 → 0.3.47

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.
Files changed (77) hide show
  1. package/bin/validate-better-sqlite3.cjs +55 -0
  2. package/dist/commands/config/index.js +39 -1
  3. package/dist/commands/linear/auth.d.ts +14 -0
  4. package/dist/commands/linear/auth.js +211 -0
  5. package/dist/commands/linear/import.d.ts +21 -0
  6. package/dist/commands/linear/import.js +260 -0
  7. package/dist/commands/linear/status.d.ts +11 -0
  8. package/dist/commands/linear/status.js +88 -0
  9. package/dist/commands/linear/sync.d.ts +15 -0
  10. package/dist/commands/linear/sync.js +233 -0
  11. package/dist/commands/orchestrator/attach.d.ts +10 -1
  12. package/dist/commands/orchestrator/attach.js +102 -18
  13. package/dist/commands/orchestrator/index.js +22 -7
  14. package/dist/commands/orchestrator/start.d.ts +13 -1
  15. package/dist/commands/orchestrator/start.js +96 -25
  16. package/dist/commands/orchestrator/status.d.ts +1 -0
  17. package/dist/commands/orchestrator/status.js +10 -5
  18. package/dist/commands/orchestrator/stop.d.ts +1 -0
  19. package/dist/commands/orchestrator/stop.js +9 -4
  20. package/dist/commands/session/attach.js +32 -9
  21. package/dist/commands/ticket/link/duplicates.d.ts +15 -0
  22. package/dist/commands/ticket/link/duplicates.js +95 -0
  23. package/dist/commands/ticket/link/index.js +14 -0
  24. package/dist/commands/ticket/link/relates.d.ts +15 -0
  25. package/dist/commands/ticket/link/relates.js +95 -0
  26. package/dist/commands/work/index.js +4 -0
  27. package/dist/commands/work/review.d.ts +45 -0
  28. package/dist/commands/work/review.js +401 -0
  29. package/dist/commands/work/revise.js +4 -3
  30. package/dist/commands/work/spawn.d.ts +5 -0
  31. package/dist/commands/work/spawn.js +195 -14
  32. package/dist/commands/work/start.js +75 -19
  33. package/dist/hooks/init.js +18 -5
  34. package/dist/lib/database/native-validation.d.ts +21 -0
  35. package/dist/lib/database/native-validation.js +49 -0
  36. package/dist/lib/execution/config.d.ts +15 -0
  37. package/dist/lib/execution/config.js +54 -0
  38. package/dist/lib/execution/devcontainer.d.ts +6 -3
  39. package/dist/lib/execution/devcontainer.js +39 -12
  40. package/dist/lib/execution/runners.d.ts +28 -32
  41. package/dist/lib/execution/runners.js +353 -277
  42. package/dist/lib/execution/spawner.js +62 -5
  43. package/dist/lib/execution/types.d.ts +4 -0
  44. package/dist/lib/execution/types.js +3 -0
  45. package/dist/lib/external-issues/adapters.d.ts +26 -0
  46. package/dist/lib/external-issues/adapters.js +251 -0
  47. package/dist/lib/external-issues/index.d.ts +10 -0
  48. package/dist/lib/external-issues/index.js +14 -0
  49. package/dist/lib/external-issues/mapper.d.ts +21 -0
  50. package/dist/lib/external-issues/mapper.js +86 -0
  51. package/dist/lib/external-issues/types.d.ts +144 -0
  52. package/dist/lib/external-issues/types.js +26 -0
  53. package/dist/lib/external-issues/validation.d.ts +34 -0
  54. package/dist/lib/external-issues/validation.js +219 -0
  55. package/dist/lib/linear/client.d.ts +55 -0
  56. package/dist/lib/linear/client.js +254 -0
  57. package/dist/lib/linear/config.d.ts +37 -0
  58. package/dist/lib/linear/config.js +100 -0
  59. package/dist/lib/linear/index.d.ts +11 -0
  60. package/dist/lib/linear/index.js +10 -0
  61. package/dist/lib/linear/mapper.d.ts +67 -0
  62. package/dist/lib/linear/mapper.js +219 -0
  63. package/dist/lib/linear/sync.d.ts +37 -0
  64. package/dist/lib/linear/sync.js +89 -0
  65. package/dist/lib/linear/types.d.ts +139 -0
  66. package/dist/lib/linear/types.js +34 -0
  67. package/dist/lib/mcp/helpers.d.ts +8 -0
  68. package/dist/lib/mcp/helpers.js +10 -0
  69. package/dist/lib/mcp/tools/board.js +63 -11
  70. package/dist/lib/mcp/tools/work.js +36 -0
  71. package/dist/lib/pmo/schema.d.ts +2 -0
  72. package/dist/lib/pmo/schema.js +20 -0
  73. package/dist/lib/pmo/storage/base.js +92 -13
  74. package/dist/lib/pmo/storage/dependencies.js +15 -0
  75. package/dist/lib/prompt-json.d.ts +4 -0
  76. package/oclif.manifest.json +3205 -2537
  77. package/package.json +3 -2
@@ -0,0 +1,26 @@
1
+ /**
2
+ * External Issue Adapter Types
3
+ *
4
+ * Canonical types for normalizing issues from external sources
5
+ * (Linear and Jira) into a shared IssueEnvelope format
6
+ * that can be mapped to spawn context.
7
+ */
8
+ /**
9
+ * All valid issue sources as a const array.
10
+ */
11
+ export const ISSUE_SOURCES = ['linear', 'jira'];
12
+ /**
13
+ * Typed error for external issue operations.
14
+ */
15
+ export class ExternalIssueError extends Error {
16
+ code;
17
+ source;
18
+ validationErrors;
19
+ constructor(code, message, source, validationErrors) {
20
+ super(message);
21
+ this.code = code;
22
+ this.source = source;
23
+ this.validationErrors = validationErrors;
24
+ this.name = 'ExternalIssueError';
25
+ }
26
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * IssueEnvelope Validation
3
+ *
4
+ * Validates raw data into a canonical IssueEnvelope.
5
+ * Provides typed validation errors for missing or invalid fields.
6
+ */
7
+ import { type IssueEnvelope, type IssueValidationResult } from './types.js';
8
+ /**
9
+ * Validate and construct an IssueEnvelope from untyped input.
10
+ *
11
+ * Checks:
12
+ * - `source` is a valid IssueSource
13
+ * - Required string fields are present and non-empty
14
+ * - `description` is present (may be empty string)
15
+ * - `labels` is an array of strings
16
+ * - `priority` is a string or null
17
+ * - `assignee` is a string or null
18
+ * - `item_type` is optional, but if present must be a non-empty string or null
19
+ * - `raw` is a plain object
20
+ *
21
+ * @param input - Untyped input to validate
22
+ * @returns Validation result with either the valid envelope or an array of errors
23
+ */
24
+ export declare function validateIssueEnvelope(input: unknown): IssueValidationResult;
25
+ /**
26
+ * Validate an IssueEnvelope or throw an ExternalIssueError.
27
+ *
28
+ * Convenience wrapper around validateIssueEnvelope that throws on failure.
29
+ *
30
+ * @param input - Untyped input to validate
31
+ * @returns Valid IssueEnvelope
32
+ * @throws ExternalIssueError with code 'VALIDATION_FAILED' if invalid
33
+ */
34
+ export declare function validateOrThrow(input: unknown): IssueEnvelope;
@@ -0,0 +1,219 @@
1
+ /**
2
+ * IssueEnvelope Validation
3
+ *
4
+ * Validates raw data into a canonical IssueEnvelope.
5
+ * Provides typed validation errors for missing or invalid fields.
6
+ */
7
+ import { ISSUE_SOURCES, ExternalIssueError, } from './types.js';
8
+ /**
9
+ * Required string fields on IssueEnvelope (must be present and non-empty).
10
+ */
11
+ const REQUIRED_STRING_FIELDS = [
12
+ 'external_id',
13
+ 'external_key',
14
+ 'title',
15
+ 'status',
16
+ 'url',
17
+ 'project_key',
18
+ ];
19
+ /**
20
+ * Validate and construct an IssueEnvelope from untyped input.
21
+ *
22
+ * Checks:
23
+ * - `source` is a valid IssueSource
24
+ * - Required string fields are present and non-empty
25
+ * - `description` is present (may be empty string)
26
+ * - `labels` is an array of strings
27
+ * - `priority` is a string or null
28
+ * - `assignee` is a string or null
29
+ * - `item_type` is optional, but if present must be a non-empty string or null
30
+ * - `raw` is a plain object
31
+ *
32
+ * @param input - Untyped input to validate
33
+ * @returns Validation result with either the valid envelope or an array of errors
34
+ */
35
+ export function validateIssueEnvelope(input) {
36
+ const errors = [];
37
+ if (typeof input !== 'object' || input === null) {
38
+ errors.push({
39
+ code: 'INVALID_FIELD_TYPE',
40
+ field: '(root)',
41
+ message: 'Input must be a non-null object',
42
+ });
43
+ return { valid: false, errors };
44
+ }
45
+ const data = input;
46
+ // Validate source
47
+ if (!('source' in data) || data.source === undefined || data.source === null) {
48
+ errors.push({
49
+ code: 'MISSING_FIELD',
50
+ field: 'source',
51
+ message: 'source is required',
52
+ });
53
+ }
54
+ else if (typeof data.source !== 'string') {
55
+ errors.push({
56
+ code: 'INVALID_FIELD_TYPE',
57
+ field: 'source',
58
+ message: 'source must be a string',
59
+ });
60
+ }
61
+ else if (!ISSUE_SOURCES.includes(data.source)) {
62
+ errors.push({
63
+ code: 'INVALID_SOURCE',
64
+ field: 'source',
65
+ message: `source must be one of: ${ISSUE_SOURCES.join(', ')}. Got: "${data.source}"`,
66
+ });
67
+ }
68
+ // Validate required string fields
69
+ for (const field of REQUIRED_STRING_FIELDS) {
70
+ if (!(field in data) || data[field] === undefined || data[field] === null) {
71
+ errors.push({
72
+ code: 'MISSING_FIELD',
73
+ field,
74
+ message: `${field} is required`,
75
+ });
76
+ }
77
+ else if (typeof data[field] !== 'string') {
78
+ errors.push({
79
+ code: 'INVALID_FIELD_TYPE',
80
+ field,
81
+ message: `${field} must be a string`,
82
+ });
83
+ }
84
+ else if (data[field].trim() === '') {
85
+ errors.push({
86
+ code: 'EMPTY_FIELD',
87
+ field,
88
+ message: `${field} must not be empty`,
89
+ });
90
+ }
91
+ }
92
+ // Validate description (required, but may be empty string)
93
+ if (!('description' in data) || data.description === undefined || data.description === null) {
94
+ errors.push({
95
+ code: 'MISSING_FIELD',
96
+ field: 'description',
97
+ message: 'description is required',
98
+ });
99
+ }
100
+ else if (typeof data.description !== 'string') {
101
+ errors.push({
102
+ code: 'INVALID_FIELD_TYPE',
103
+ field: 'description',
104
+ message: 'description must be a string',
105
+ });
106
+ }
107
+ // Validate labels
108
+ if (!('labels' in data) || data.labels === undefined || data.labels === null) {
109
+ errors.push({
110
+ code: 'MISSING_FIELD',
111
+ field: 'labels',
112
+ message: 'labels is required',
113
+ });
114
+ }
115
+ else if (!Array.isArray(data.labels)) {
116
+ errors.push({
117
+ code: 'INVALID_FIELD_TYPE',
118
+ field: 'labels',
119
+ message: 'labels must be an array',
120
+ });
121
+ }
122
+ else {
123
+ for (let i = 0; i < data.labels.length; i++) {
124
+ if (typeof data.labels[i] !== 'string') {
125
+ errors.push({
126
+ code: 'INVALID_FIELD_TYPE',
127
+ field: `labels[${i}]`,
128
+ message: `labels[${i}] must be a string`,
129
+ });
130
+ }
131
+ }
132
+ }
133
+ // Validate priority (string or null)
134
+ if (!('priority' in data) || data.priority === undefined) {
135
+ errors.push({
136
+ code: 'MISSING_FIELD',
137
+ field: 'priority',
138
+ message: 'priority is required (use null if not set)',
139
+ });
140
+ }
141
+ else if (data.priority !== null && typeof data.priority !== 'string') {
142
+ errors.push({
143
+ code: 'INVALID_FIELD_TYPE',
144
+ field: 'priority',
145
+ message: 'priority must be a string or null',
146
+ });
147
+ }
148
+ // Validate assignee (string or null)
149
+ if (!('assignee' in data) || data.assignee === undefined) {
150
+ errors.push({
151
+ code: 'MISSING_FIELD',
152
+ field: 'assignee',
153
+ message: 'assignee is required (use null if not assigned)',
154
+ });
155
+ }
156
+ else if (data.assignee !== null && typeof data.assignee !== 'string') {
157
+ errors.push({
158
+ code: 'INVALID_FIELD_TYPE',
159
+ field: 'assignee',
160
+ message: 'assignee must be a string or null',
161
+ });
162
+ }
163
+ // Validate item_type (optional: string or null)
164
+ if ('item_type' in data && data.item_type !== undefined) {
165
+ if (data.item_type !== null && typeof data.item_type !== 'string') {
166
+ errors.push({
167
+ code: 'INVALID_FIELD_TYPE',
168
+ field: 'item_type',
169
+ message: 'item_type must be a string or null',
170
+ });
171
+ }
172
+ else if (typeof data.item_type === 'string' && data.item_type.trim() === '') {
173
+ errors.push({
174
+ code: 'EMPTY_FIELD',
175
+ field: 'item_type',
176
+ message: 'item_type must not be empty when provided',
177
+ });
178
+ }
179
+ }
180
+ // Validate raw
181
+ if (!('raw' in data) || data.raw === undefined || data.raw === null) {
182
+ errors.push({
183
+ code: 'MISSING_FIELD',
184
+ field: 'raw',
185
+ message: 'raw is required',
186
+ });
187
+ }
188
+ else if (typeof data.raw !== 'object' || Array.isArray(data.raw)) {
189
+ errors.push({
190
+ code: 'INVALID_FIELD_TYPE',
191
+ field: 'raw',
192
+ message: 'raw must be a plain object',
193
+ });
194
+ }
195
+ if (errors.length > 0) {
196
+ return { valid: false, errors };
197
+ }
198
+ return {
199
+ valid: true,
200
+ envelope: data,
201
+ };
202
+ }
203
+ /**
204
+ * Validate an IssueEnvelope or throw an ExternalIssueError.
205
+ *
206
+ * Convenience wrapper around validateIssueEnvelope that throws on failure.
207
+ *
208
+ * @param input - Untyped input to validate
209
+ * @returns Valid IssueEnvelope
210
+ * @throws ExternalIssueError with code 'VALIDATION_FAILED' if invalid
211
+ */
212
+ export function validateOrThrow(input) {
213
+ const result = validateIssueEnvelope(input);
214
+ if (!result.valid) {
215
+ const fieldList = result.errors.map((e) => e.field).join(', ');
216
+ throw new ExternalIssueError('VALIDATION_FAILED', `Issue envelope validation failed: invalid fields [${fieldList}]`, undefined, result.errors);
217
+ }
218
+ return result.envelope;
219
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Linear API Client
3
+ *
4
+ * Thin wrapper around @linear/sdk providing typed access to
5
+ * Linear issues, teams, states, and cycles for the PMO integration.
6
+ */
7
+ import type { LinearIssue, LinearTeam, LinearWorkflowState, LinearCycle, LinearIssueFilter } from './types.js';
8
+ export declare class LinearClient {
9
+ private sdk;
10
+ constructor(apiKey: string);
11
+ /**
12
+ * Verify the API key is valid and return the authenticated user's organization.
13
+ */
14
+ verify(): Promise<{
15
+ organizationName: string;
16
+ userName: string;
17
+ email: string;
18
+ }>;
19
+ /**
20
+ * List all teams in the workspace.
21
+ */
22
+ listTeams(): Promise<LinearTeam[]>;
23
+ /**
24
+ * Get a team by its key (e.g., "ENG").
25
+ */
26
+ getTeamByKey(key: string): Promise<LinearTeam | null>;
27
+ /**
28
+ * List workflow states for a team.
29
+ */
30
+ listStates(teamId: string): Promise<LinearWorkflowState[]>;
31
+ /**
32
+ * List cycles for a team.
33
+ */
34
+ listCycles(teamId: string): Promise<LinearCycle[]>;
35
+ /**
36
+ * Fetch issues from Linear with optional filters.
37
+ */
38
+ listIssues(filter?: LinearIssueFilter): Promise<LinearIssue[]>;
39
+ /**
40
+ * Fetch a single issue by its identifier (e.g., "ENG-123").
41
+ */
42
+ getIssueByIdentifier(identifier: string): Promise<LinearIssue | null>;
43
+ /**
44
+ * Update the state of an issue.
45
+ */
46
+ updateIssueState(issueId: string, stateId: string): Promise<void>;
47
+ /**
48
+ * Add a comment to an issue.
49
+ */
50
+ addComment(issueId: string, body: string): Promise<void>;
51
+ /**
52
+ * Attach a URL to an issue (e.g., a PR link).
53
+ */
54
+ attachUrl(issueId: string, url: string, title: string): Promise<void>;
55
+ }
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Linear API Client
3
+ *
4
+ * Thin wrapper around @linear/sdk providing typed access to
5
+ * Linear issues, teams, states, and cycles for the PMO integration.
6
+ */
7
+ import { LinearClient as SDKClient } from '@linear/sdk';
8
+ export class LinearClient {
9
+ sdk;
10
+ constructor(apiKey) {
11
+ this.sdk = new SDKClient({ apiKey });
12
+ }
13
+ /**
14
+ * Verify the API key is valid and return the authenticated user's organization.
15
+ */
16
+ async verify() {
17
+ const viewer = await this.sdk.viewer;
18
+ const org = await this.sdk.organization;
19
+ return {
20
+ organizationName: org.name,
21
+ userName: viewer.name,
22
+ email: viewer.email,
23
+ };
24
+ }
25
+ /**
26
+ * List all teams in the workspace.
27
+ */
28
+ async listTeams() {
29
+ const teams = await this.sdk.teams();
30
+ return teams.nodes.map((t) => ({
31
+ id: t.id,
32
+ key: t.key,
33
+ name: t.name,
34
+ description: t.description ?? undefined,
35
+ }));
36
+ }
37
+ /**
38
+ * Get a team by its key (e.g., "ENG").
39
+ */
40
+ async getTeamByKey(key) {
41
+ const teams = await this.listTeams();
42
+ return teams.find((t) => t.key.toLowerCase() === key.toLowerCase()) ?? null;
43
+ }
44
+ /**
45
+ * List workflow states for a team.
46
+ */
47
+ async listStates(teamId) {
48
+ const team = await this.sdk.team(teamId);
49
+ const states = await team.states();
50
+ return states.nodes.map((s) => ({
51
+ id: s.id,
52
+ name: s.name,
53
+ type: s.type,
54
+ color: s.color,
55
+ position: s.position,
56
+ }));
57
+ }
58
+ /**
59
+ * List cycles for a team.
60
+ */
61
+ async listCycles(teamId) {
62
+ const team = await this.sdk.team(teamId);
63
+ const cycles = await team.cycles();
64
+ return cycles.nodes.map((c) => ({
65
+ id: c.id,
66
+ name: c.name ?? `Cycle ${c.number}`,
67
+ number: c.number,
68
+ startsAt: c.startsAt?.toISOString() ?? '',
69
+ endsAt: c.endsAt?.toISOString() ?? '',
70
+ }));
71
+ }
72
+ /**
73
+ * Fetch issues from Linear with optional filters.
74
+ */
75
+ async listIssues(filter = {}) {
76
+ const queryFilter = {};
77
+ if (filter.teamId) {
78
+ queryFilter.team = { id: { eq: filter.teamId } };
79
+ }
80
+ else if (filter.teamKey) {
81
+ queryFilter.team = { key: { eq: filter.teamKey } };
82
+ }
83
+ if (filter.stateType) {
84
+ queryFilter.state = { type: { eq: filter.stateType } };
85
+ }
86
+ else if (filter.stateName) {
87
+ queryFilter.state = { name: { eqIgnoreCase: filter.stateName } };
88
+ }
89
+ if (filter.assigneeMe) {
90
+ const viewer = await this.sdk.viewer;
91
+ queryFilter.assignee = { id: { eq: viewer.id } };
92
+ }
93
+ else if (filter.assigneeId) {
94
+ queryFilter.assignee = { id: { eq: filter.assigneeId } };
95
+ }
96
+ if (filter.labelName) {
97
+ queryFilter.labels = { name: { eqIgnoreCase: filter.labelName } };
98
+ }
99
+ if (filter.cycleId) {
100
+ queryFilter.cycle = { id: { eq: filter.cycleId } };
101
+ }
102
+ if (filter.projectId) {
103
+ queryFilter.project = { id: { eq: filter.projectId } };
104
+ }
105
+ const issues = await this.sdk.issues({
106
+ filter: queryFilter,
107
+ first: filter.limit ?? 50,
108
+ });
109
+ const results = [];
110
+ for (const issue of issues.nodes) {
111
+ // eslint-disable-next-line no-await-in-loop
112
+ const state = await issue.state;
113
+ // eslint-disable-next-line no-await-in-loop
114
+ const team = await issue.team;
115
+ // eslint-disable-next-line no-await-in-loop
116
+ const assignee = await issue.assignee;
117
+ // eslint-disable-next-line no-await-in-loop
118
+ const labelsConn = await issue.labels();
119
+ // eslint-disable-next-line no-await-in-loop
120
+ const cycle = await issue.cycle;
121
+ // eslint-disable-next-line no-await-in-loop
122
+ const project = await issue.project;
123
+ results.push({
124
+ id: issue.id,
125
+ identifier: issue.identifier,
126
+ title: issue.title,
127
+ description: issue.description ?? undefined,
128
+ priority: issue.priority,
129
+ state: state ? {
130
+ id: state.id,
131
+ name: state.name,
132
+ type: state.type,
133
+ } : { id: '', name: 'Unknown', type: 'backlog' },
134
+ team: team ? {
135
+ id: team.id,
136
+ key: team.key,
137
+ name: team.name,
138
+ } : { id: '', key: '', name: 'Unknown' },
139
+ assignee: assignee ? {
140
+ id: assignee.id,
141
+ name: assignee.name,
142
+ email: assignee.email,
143
+ } : undefined,
144
+ labels: labelsConn.nodes.map((l) => ({
145
+ id: l.id,
146
+ name: l.name,
147
+ color: l.color,
148
+ })),
149
+ cycle: cycle ? {
150
+ id: cycle.id,
151
+ name: cycle.name ?? `Cycle ${cycle.number}`,
152
+ number: cycle.number,
153
+ } : undefined,
154
+ project: project ? {
155
+ id: project.id,
156
+ name: project.name,
157
+ } : undefined,
158
+ estimate: issue.estimate ?? undefined,
159
+ url: issue.url,
160
+ createdAt: issue.createdAt.toISOString(),
161
+ updatedAt: issue.updatedAt.toISOString(),
162
+ });
163
+ }
164
+ return results;
165
+ }
166
+ /**
167
+ * Fetch a single issue by its identifier (e.g., "ENG-123").
168
+ */
169
+ async getIssueByIdentifier(identifier) {
170
+ // Parse team key from identifier
171
+ const match = identifier.match(/^([A-Z]+)-(\d+)$/i);
172
+ if (!match)
173
+ return null;
174
+ const [, teamKey, numberStr] = match;
175
+ const issues = await this.sdk.issues({
176
+ filter: {
177
+ team: { key: { eq: teamKey.toUpperCase() } },
178
+ number: { eq: parseInt(numberStr, 10) },
179
+ },
180
+ first: 1,
181
+ });
182
+ if (issues.nodes.length === 0)
183
+ return null;
184
+ const issue = issues.nodes[0];
185
+ const state = await issue.state;
186
+ const team = await issue.team;
187
+ const assignee = await issue.assignee;
188
+ const labelsConn = await issue.labels();
189
+ const cycle = await issue.cycle;
190
+ const project = await issue.project;
191
+ return {
192
+ id: issue.id,
193
+ identifier: issue.identifier,
194
+ title: issue.title,
195
+ description: issue.description ?? undefined,
196
+ priority: issue.priority,
197
+ state: state ? {
198
+ id: state.id,
199
+ name: state.name,
200
+ type: state.type,
201
+ } : { id: '', name: 'Unknown', type: 'backlog' },
202
+ team: team ? {
203
+ id: team.id,
204
+ key: team.key,
205
+ name: team.name,
206
+ } : { id: '', key: '', name: 'Unknown' },
207
+ assignee: assignee ? {
208
+ id: assignee.id,
209
+ name: assignee.name,
210
+ email: assignee.email,
211
+ } : undefined,
212
+ labels: labelsConn.nodes.map((l) => ({
213
+ id: l.id,
214
+ name: l.name,
215
+ color: l.color,
216
+ })),
217
+ cycle: cycle ? {
218
+ id: cycle.id,
219
+ name: cycle.name ?? `Cycle ${cycle.number}`,
220
+ number: cycle.number,
221
+ } : undefined,
222
+ project: project ? {
223
+ id: project.id,
224
+ name: project.name,
225
+ } : undefined,
226
+ estimate: issue.estimate ?? undefined,
227
+ url: issue.url,
228
+ createdAt: issue.createdAt.toISOString(),
229
+ updatedAt: issue.updatedAt.toISOString(),
230
+ };
231
+ }
232
+ /**
233
+ * Update the state of an issue.
234
+ */
235
+ async updateIssueState(issueId, stateId) {
236
+ await this.sdk.updateIssue(issueId, { stateId });
237
+ }
238
+ /**
239
+ * Add a comment to an issue.
240
+ */
241
+ async addComment(issueId, body) {
242
+ await this.sdk.createComment({ issueId, body });
243
+ }
244
+ /**
245
+ * Attach a URL to an issue (e.g., a PR link).
246
+ */
247
+ async attachUrl(issueId, url, title) {
248
+ await this.sdk.createAttachment({
249
+ issueId,
250
+ url,
251
+ title,
252
+ });
253
+ }
254
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Linear Configuration Storage
3
+ *
4
+ * Stores Linear credentials and preferences in the workspace_settings table.
5
+ */
6
+ import Database from 'better-sqlite3';
7
+ import type { LinearConfig } from './types.js';
8
+ /**
9
+ * Check if Linear is configured (API key is stored).
10
+ */
11
+ export declare function isLinearConfigured(db: Database.Database): boolean;
12
+ /**
13
+ * Load Linear configuration from the database.
14
+ * Returns null if not configured.
15
+ */
16
+ export declare function loadLinearConfig(db: Database.Database): LinearConfig | null;
17
+ /**
18
+ * Save Linear API key to the database.
19
+ */
20
+ export declare function saveLinearApiKey(db: Database.Database, apiKey: string): void;
21
+ /**
22
+ * Save the default team for Linear operations.
23
+ */
24
+ export declare function saveLinearDefaultTeam(db: Database.Database, teamId: string, teamKey: string): void;
25
+ /**
26
+ * Save the organization name.
27
+ */
28
+ export declare function saveLinearOrganization(db: Database.Database, name: string): void;
29
+ /**
30
+ * Clear all Linear configuration from the database.
31
+ */
32
+ export declare function clearLinearConfig(db: Database.Database): void;
33
+ /**
34
+ * Get the stored Linear API key.
35
+ * Also checks PRLT_LINEAR_API_KEY and LINEAR_API_KEY environment variables.
36
+ */
37
+ export declare function getLinearApiKey(db: Database.Database): string | null;