linear-cli-agents 0.1.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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +168 -0
  3. package/bin/dev.js +5 -0
  4. package/bin/run.js +5 -0
  5. package/dist/commands/auth/login.d.ts +12 -0
  6. package/dist/commands/auth/login.js +52 -0
  7. package/dist/commands/auth/logout.d.ts +6 -0
  8. package/dist/commands/auth/logout.js +26 -0
  9. package/dist/commands/auth/status.d.ts +6 -0
  10. package/dist/commands/auth/status.js +40 -0
  11. package/dist/commands/issues/create.d.ts +18 -0
  12. package/dist/commands/issues/create.js +109 -0
  13. package/dist/commands/issues/delete.d.ts +12 -0
  14. package/dist/commands/issues/delete.js +61 -0
  15. package/dist/commands/issues/get.d.ts +9 -0
  16. package/dist/commands/issues/get.js +81 -0
  17. package/dist/commands/issues/list.d.ts +14 -0
  18. package/dist/commands/issues/list.js +101 -0
  19. package/dist/commands/issues/update.d.ts +20 -0
  20. package/dist/commands/issues/update.js +110 -0
  21. package/dist/commands/query.d.ts +11 -0
  22. package/dist/commands/query.js +94 -0
  23. package/dist/commands/schema.d.ts +13 -0
  24. package/dist/commands/schema.js +198 -0
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.js +1 -0
  27. package/dist/lib/client.d.ts +17 -0
  28. package/dist/lib/client.js +23 -0
  29. package/dist/lib/config.d.ts +33 -0
  30. package/dist/lib/config.js +97 -0
  31. package/dist/lib/errors.d.ts +30 -0
  32. package/dist/lib/errors.js +79 -0
  33. package/dist/lib/issue-utils.d.ts +17 -0
  34. package/dist/lib/issue-utils.js +56 -0
  35. package/dist/lib/output.d.ts +16 -0
  36. package/dist/lib/output.js +33 -0
  37. package/dist/lib/types.d.ts +33 -0
  38. package/dist/lib/types.js +5 -0
  39. package/oclif.manifest.json +549 -0
  40. package/package.json +69 -0
@@ -0,0 +1,101 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { getClient } from '../../lib/client.js';
3
+ import { successList, print } from '../../lib/output.js';
4
+ import { handleError } from '../../lib/errors.js';
5
+ export default class IssuesList extends Command {
6
+ static description = 'List issues with optional filtering';
7
+ static examples = [
8
+ '<%= config.bin %> issues list',
9
+ '<%= config.bin %> issues list --team ENG',
10
+ '<%= config.bin %> issues list --assignee me',
11
+ '<%= config.bin %> issues list --filter \'{"state":{"name":{"eq":"In Progress"}}}\'',
12
+ '<%= config.bin %> issues list --first 50 --after cursor123',
13
+ ];
14
+ static flags = {
15
+ team: Flags.string({
16
+ char: 't',
17
+ description: 'Filter by team key (e.g., ENG)',
18
+ }),
19
+ assignee: Flags.string({
20
+ char: 'a',
21
+ description: 'Filter by assignee (use "me" for current user)',
22
+ }),
23
+ state: Flags.string({
24
+ char: 's',
25
+ description: 'Filter by state name (e.g., "In Progress")',
26
+ }),
27
+ filter: Flags.string({
28
+ char: 'f',
29
+ description: 'JSON filter object (IssueFilter from Linear SDK)',
30
+ }),
31
+ first: Flags.integer({
32
+ description: 'Number of issues to fetch (default: 50)',
33
+ default: 50,
34
+ }),
35
+ after: Flags.string({
36
+ description: 'Cursor for pagination',
37
+ }),
38
+ };
39
+ async run() {
40
+ try {
41
+ const { flags } = await this.parse(IssuesList);
42
+ const client = getClient();
43
+ // Build filter
44
+ let filter = {};
45
+ // Parse JSON filter if provided
46
+ if (flags.filter) {
47
+ try {
48
+ filter = JSON.parse(flags.filter);
49
+ }
50
+ catch {
51
+ throw new Error('Invalid JSON in --filter flag');
52
+ }
53
+ }
54
+ // Apply shorthand filters
55
+ if (flags.team) {
56
+ filter.team = { key: { eq: flags.team } };
57
+ }
58
+ if (flags.assignee) {
59
+ if (flags.assignee === 'me') {
60
+ const viewer = await client.viewer;
61
+ filter.assignee = { id: { eq: viewer.id } };
62
+ }
63
+ else {
64
+ filter.assignee = { id: { eq: flags.assignee } };
65
+ }
66
+ }
67
+ if (flags.state) {
68
+ filter.state = { name: { eq: flags.state } };
69
+ }
70
+ // Fetch issues
71
+ const issues = await client.issues({
72
+ filter,
73
+ first: flags.first,
74
+ after: flags.after,
75
+ });
76
+ // Map to clean objects
77
+ const data = issues.nodes.map((issue) => ({
78
+ id: issue.id,
79
+ identifier: issue.identifier,
80
+ title: issue.title,
81
+ description: issue.description,
82
+ priority: issue.priority,
83
+ priorityLabel: issue.priorityLabel,
84
+ estimate: issue.estimate,
85
+ url: issue.url,
86
+ createdAt: issue.createdAt,
87
+ updatedAt: issue.updatedAt,
88
+ }));
89
+ print(successList(data, {
90
+ hasNextPage: issues.pageInfo.hasNextPage,
91
+ hasPreviousPage: issues.pageInfo.hasPreviousPage,
92
+ startCursor: issues.pageInfo.startCursor,
93
+ endCursor: issues.pageInfo.endCursor,
94
+ }));
95
+ }
96
+ catch (err) {
97
+ handleError(err);
98
+ this.exit(1);
99
+ }
100
+ }
101
+ }
@@ -0,0 +1,20 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class IssuesUpdate extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ id: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ input: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ title: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ description: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ priority: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ 'assignee-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ 'state-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ 'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
+ estimate: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
17
+ 'label-ids': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
18
+ };
19
+ run(): Promise<void>;
20
+ }
@@ -0,0 +1,110 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { getClient } from '../../lib/client.js';
3
+ import { success, print } from '../../lib/output.js';
4
+ import { handleError, CliError, ErrorCodes } from '../../lib/errors.js';
5
+ import { resolveIssueId } from '../../lib/issue-utils.js';
6
+ export default class IssuesUpdate extends Command {
7
+ static description = 'Update an existing issue';
8
+ static examples = [
9
+ '<%= config.bin %> issues update abc123 --input \'{"title":"Updated title"}\'',
10
+ '<%= config.bin %> issues update ENG-123 --title "New title"',
11
+ '<%= config.bin %> issues update ENG-123 --state-id xxx --assignee-id yyy',
12
+ ];
13
+ static args = {
14
+ id: Args.string({
15
+ description: 'Issue ID or identifier (e.g., ENG-123)',
16
+ required: true,
17
+ }),
18
+ };
19
+ static flags = {
20
+ input: Flags.string({
21
+ char: 'i',
22
+ description: 'JSON input object (IssueUpdateInput)',
23
+ exclusive: ['title', 'description', 'priority', 'assignee-id', 'state-id'],
24
+ }),
25
+ title: Flags.string({
26
+ description: 'Issue title',
27
+ }),
28
+ description: Flags.string({
29
+ char: 'd',
30
+ description: 'Issue description (markdown supported)',
31
+ }),
32
+ priority: Flags.integer({
33
+ char: 'p',
34
+ description: 'Priority (0=none, 1=urgent, 2=high, 3=medium, 4=low)',
35
+ }),
36
+ 'assignee-id': Flags.string({
37
+ description: 'Assignee user ID (use empty string to unassign)',
38
+ }),
39
+ 'state-id': Flags.string({
40
+ description: 'State ID',
41
+ }),
42
+ 'project-id': Flags.string({
43
+ description: 'Project ID',
44
+ }),
45
+ estimate: Flags.integer({
46
+ description: 'Estimate points',
47
+ }),
48
+ 'label-ids': Flags.string({
49
+ description: 'Comma-separated label IDs (replaces existing labels)',
50
+ }),
51
+ };
52
+ async run() {
53
+ try {
54
+ const { args, flags } = await this.parse(IssuesUpdate);
55
+ const client = getClient();
56
+ const issueId = await resolveIssueId(client, args.id);
57
+ let input;
58
+ if (flags.input) {
59
+ // Parse JSON input
60
+ try {
61
+ input = JSON.parse(flags.input);
62
+ }
63
+ catch {
64
+ throw new CliError(ErrorCodes.INVALID_INPUT, 'Invalid JSON in --input flag');
65
+ }
66
+ }
67
+ else {
68
+ // Build input from individual flags
69
+ input = {};
70
+ if (flags.title)
71
+ input.title = flags.title;
72
+ if (flags.description)
73
+ input.description = flags.description;
74
+ if (flags.priority !== undefined)
75
+ input.priority = flags.priority;
76
+ if (flags['assignee-id'] !== undefined) {
77
+ input.assigneeId = flags['assignee-id'] || null;
78
+ }
79
+ if (flags['state-id'])
80
+ input.stateId = flags['state-id'];
81
+ if (flags['project-id'])
82
+ input.projectId = flags['project-id'];
83
+ if (flags.estimate !== undefined)
84
+ input.estimate = flags.estimate;
85
+ if (flags['label-ids'])
86
+ input.labelIds = flags['label-ids'].split(',');
87
+ if (Object.keys(input).length === 0) {
88
+ throw new CliError(ErrorCodes.INVALID_INPUT, 'No update fields provided. Use --input or individual flags.');
89
+ }
90
+ }
91
+ // Update the issue
92
+ const payload = await client.updateIssue(issueId, input);
93
+ const issue = await payload.issue;
94
+ if (!issue) {
95
+ throw new CliError(ErrorCodes.API_ERROR, 'Failed to update issue');
96
+ }
97
+ print(success({
98
+ id: issue.id,
99
+ identifier: issue.identifier,
100
+ title: issue.title,
101
+ url: issue.url,
102
+ updatedAt: issue.updatedAt,
103
+ }));
104
+ }
105
+ catch (err) {
106
+ handleError(err);
107
+ this.exit(1);
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,11 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Query extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ gql: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ variables: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ timeout: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
9
+ };
10
+ run(): Promise<void>;
11
+ }
@@ -0,0 +1,94 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { requireApiKey } from '../lib/config.js';
3
+ import { success, error, print } from '../lib/output.js';
4
+ import { handleError, CliError, ErrorCodes } from '../lib/errors.js';
5
+ const LINEAR_API_URL = 'https://api.linear.app/graphql';
6
+ export default class Query extends Command {
7
+ static description = 'Execute a raw GraphQL query against the Linear API';
8
+ static examples = [
9
+ '<%= config.bin %> query --gql "query { viewer { id name email } }"',
10
+ '<%= config.bin %> query --gql "query { teams { nodes { id key name } } }"',
11
+ '<%= config.bin %> query --gql "query($id: String!) { issue(id: $id) { title } }" --variables \'{"id":"xxx"}\'',
12
+ ];
13
+ static flags = {
14
+ gql: Flags.string({
15
+ char: 'g',
16
+ description: 'GraphQL query string',
17
+ required: true,
18
+ }),
19
+ variables: Flags.string({
20
+ char: 'v',
21
+ description: 'JSON object of query variables',
22
+ }),
23
+ timeout: Flags.integer({
24
+ char: 't',
25
+ description: 'Request timeout in seconds (default: 30)',
26
+ default: 30,
27
+ }),
28
+ };
29
+ async run() {
30
+ const { flags } = await this.parse(Query);
31
+ const controller = new AbortController();
32
+ let timeoutHandle;
33
+ const timeoutSeconds = flags.timeout ?? 30;
34
+ try {
35
+ const apiKey = requireApiKey();
36
+ let variables;
37
+ if (flags.variables) {
38
+ try {
39
+ variables = JSON.parse(flags.variables);
40
+ }
41
+ catch {
42
+ print(error('INVALID_INPUT', 'Invalid JSON in --variables flag'));
43
+ this.exit(1);
44
+ return;
45
+ }
46
+ }
47
+ timeoutHandle = setTimeout(() => controller.abort(), timeoutSeconds * 1000);
48
+ const response = await fetch(LINEAR_API_URL, {
49
+ method: 'POST',
50
+ headers: {
51
+ 'Content-Type': 'application/json',
52
+ Authorization: apiKey,
53
+ },
54
+ body: JSON.stringify({
55
+ query: flags.gql,
56
+ variables,
57
+ }),
58
+ signal: controller.signal,
59
+ });
60
+ clearTimeout(timeoutHandle);
61
+ if (!response.ok) {
62
+ print(error('API_ERROR', `Linear API returned ${response.status}: ${response.statusText}`));
63
+ this.exit(1);
64
+ return;
65
+ }
66
+ let result;
67
+ try {
68
+ result = (await response.json());
69
+ }
70
+ catch (parseError) {
71
+ throw new CliError(ErrorCodes.API_ERROR, `Failed to parse Linear API response: ${parseError instanceof Error ? parseError.message : 'Invalid JSON'}`, { status: response.status, statusText: response.statusText });
72
+ }
73
+ if (result.errors && result.errors.length > 0) {
74
+ print(error('GRAPHQL_ERROR', result.errors[0].message, {
75
+ errors: result.errors,
76
+ }));
77
+ this.exit(1);
78
+ return;
79
+ }
80
+ print(success(result.data));
81
+ }
82
+ catch (err) {
83
+ if (timeoutHandle)
84
+ clearTimeout(timeoutHandle);
85
+ if (err instanceof Error && err.name === 'AbortError') {
86
+ print(error(ErrorCodes.API_ERROR, `Request timed out after ${timeoutSeconds} seconds`));
87
+ this.exit(1);
88
+ return;
89
+ }
90
+ handleError(err);
91
+ this.exit(1);
92
+ }
93
+ }
94
+ }
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Schema extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static args: {
6
+ entity: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
7
+ };
8
+ static flags: {
9
+ full: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ 'include-examples': import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
@@ -0,0 +1,198 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { success, print } from '../lib/output.js';
3
+ /**
4
+ * Schema definitions for all supported entities.
5
+ * This enables LLMs to discover available operations and construct valid queries.
6
+ */
7
+ const ENTITY_SCHEMAS = {
8
+ issues: {
9
+ entity: 'issues',
10
+ operations: ['list', 'get', 'create', 'update', 'delete'],
11
+ description: 'Work items in Linear (bugs, features, tasks)',
12
+ fields: {
13
+ id: { type: 'ID!', description: 'Unique identifier' },
14
+ identifier: { type: 'String!', description: 'Human-readable identifier (e.g., ENG-123)' },
15
+ title: { type: 'String!', description: 'Issue title' },
16
+ description: { type: 'String', description: 'Issue description in markdown' },
17
+ priority: { type: 'Int!', description: 'Priority (0=none, 1=urgent, 2=high, 3=medium, 4=low)' },
18
+ priorityLabel: { type: 'String!', description: 'Priority label text' },
19
+ estimate: { type: 'Float', description: 'Story points estimate' },
20
+ url: { type: 'String!', description: 'Linear URL' },
21
+ createdAt: { type: 'DateTime!', description: 'Creation timestamp' },
22
+ updatedAt: { type: 'DateTime!', description: 'Last update timestamp' },
23
+ },
24
+ filters: {
25
+ team: { type: 'TeamFilter', description: 'Filter by team' },
26
+ assignee: { type: 'UserFilter', description: 'Filter by assignee' },
27
+ state: { type: 'WorkflowStateFilter', description: 'Filter by state' },
28
+ project: { type: 'ProjectFilter', description: 'Filter by project' },
29
+ priority: { type: 'IntComparator', description: 'Filter by priority' },
30
+ estimate: { type: 'EstimateComparator', description: 'Filter by estimate' },
31
+ labels: { type: 'IssueLabelFilter', description: 'Filter by labels' },
32
+ createdAt: { type: 'DateComparator', description: 'Filter by creation date' },
33
+ updatedAt: { type: 'DateComparator', description: 'Filter by update date' },
34
+ },
35
+ createInput: {
36
+ title: { type: 'String!', required: true, description: 'Issue title' },
37
+ teamId: { type: 'ID!', required: true, description: 'Team ID' },
38
+ description: { type: 'String', description: 'Issue description in markdown' },
39
+ priority: { type: 'Int', description: 'Priority (0-4)' },
40
+ assigneeId: { type: 'ID', description: 'Assignee user ID' },
41
+ stateId: { type: 'ID', description: 'Workflow state ID' },
42
+ projectId: { type: 'ID', description: 'Project ID' },
43
+ estimate: { type: 'Float', description: 'Story points estimate' },
44
+ labelIds: { type: '[ID!]', description: 'Array of label IDs' },
45
+ },
46
+ examples: {
47
+ list: 'linear issues list --team ENG --state "In Progress"',
48
+ get: 'linear issues get ENG-123',
49
+ create: 'linear issues create --title "Fix bug" --team-id xxx',
50
+ update: 'linear issues update ENG-123 --state-id yyy',
51
+ delete: 'linear issues delete ENG-123',
52
+ },
53
+ },
54
+ projects: {
55
+ entity: 'projects',
56
+ operations: ['list', 'get'],
57
+ description: 'Projects group related issues together',
58
+ fields: {
59
+ id: { type: 'ID!', description: 'Unique identifier' },
60
+ name: { type: 'String!', description: 'Project name' },
61
+ description: { type: 'String', description: 'Project description' },
62
+ state: { type: 'String!', description: 'Project state' },
63
+ url: { type: 'String!', description: 'Linear URL' },
64
+ startDate: { type: 'Date', description: 'Project start date' },
65
+ targetDate: { type: 'Date', description: 'Project target date' },
66
+ },
67
+ filters: {
68
+ name: { type: 'StringComparator', description: 'Filter by name' },
69
+ state: { type: 'StringComparator', description: 'Filter by state' },
70
+ },
71
+ },
72
+ teams: {
73
+ entity: 'teams',
74
+ operations: ['list', 'get'],
75
+ description: 'Teams organize members and issues',
76
+ fields: {
77
+ id: { type: 'ID!', description: 'Unique identifier' },
78
+ key: { type: 'String!', description: 'Team key (e.g., ENG)' },
79
+ name: { type: 'String!', description: 'Team name' },
80
+ description: { type: 'String', description: 'Team description' },
81
+ },
82
+ },
83
+ users: {
84
+ entity: 'users',
85
+ operations: ['list', 'get', 'me'],
86
+ description: 'Users in the Linear workspace',
87
+ fields: {
88
+ id: { type: 'ID!', description: 'Unique identifier' },
89
+ name: { type: 'String!', description: 'User name' },
90
+ email: { type: 'String!', description: 'User email' },
91
+ displayName: { type: 'String!', description: 'Display name' },
92
+ active: { type: 'Boolean!', description: 'Whether user is active' },
93
+ },
94
+ },
95
+ cycles: {
96
+ entity: 'cycles',
97
+ operations: ['list', 'get'],
98
+ description: 'Time-boxed iterations (sprints)',
99
+ fields: {
100
+ id: { type: 'ID!', description: 'Unique identifier' },
101
+ name: { type: 'String', description: 'Cycle name' },
102
+ number: { type: 'Int!', description: 'Cycle number' },
103
+ startsAt: { type: 'DateTime!', description: 'Cycle start date' },
104
+ endsAt: { type: 'DateTime!', description: 'Cycle end date' },
105
+ },
106
+ },
107
+ labels: {
108
+ entity: 'labels',
109
+ operations: ['list', 'get'],
110
+ description: 'Labels for categorizing issues',
111
+ fields: {
112
+ id: { type: 'ID!', description: 'Unique identifier' },
113
+ name: { type: 'String!', description: 'Label name' },
114
+ color: { type: 'String!', description: 'Label color' },
115
+ description: { type: 'String', description: 'Label description' },
116
+ },
117
+ },
118
+ comments: {
119
+ entity: 'comments',
120
+ operations: ['list', 'get', 'create'],
121
+ description: 'Comments on issues',
122
+ fields: {
123
+ id: { type: 'ID!', description: 'Unique identifier' },
124
+ body: { type: 'String!', description: 'Comment body in markdown' },
125
+ createdAt: { type: 'DateTime!', description: 'Creation timestamp' },
126
+ },
127
+ createInput: {
128
+ body: { type: 'String!', required: true, description: 'Comment body in markdown' },
129
+ issueId: { type: 'ID!', required: true, description: 'Issue ID to comment on' },
130
+ },
131
+ },
132
+ };
133
+ export default class Schema extends Command {
134
+ static description = 'Show schema information for entities (useful for LLMs to discover available operations)';
135
+ static examples = [
136
+ '<%= config.bin %> schema',
137
+ '<%= config.bin %> schema issues',
138
+ '<%= config.bin %> schema --full',
139
+ '<%= config.bin %> schema issues --include-examples',
140
+ ];
141
+ static args = {
142
+ entity: Args.string({
143
+ description: 'Entity name (issues, projects, teams, users, cycles, labels, comments)',
144
+ required: false,
145
+ }),
146
+ };
147
+ static flags = {
148
+ full: Flags.boolean({
149
+ description: 'Show full schema for all entities',
150
+ default: false,
151
+ }),
152
+ 'include-examples': Flags.boolean({
153
+ description: 'Include usage examples',
154
+ default: false,
155
+ }),
156
+ };
157
+ async run() {
158
+ const { args, flags } = await this.parse(Schema);
159
+ if (flags.full) {
160
+ // Return all schemas
161
+ print(success({
162
+ entities: Object.keys(ENTITY_SCHEMAS),
163
+ schemas: ENTITY_SCHEMAS,
164
+ note: 'Use "linear query" for raw GraphQL queries',
165
+ }));
166
+ return;
167
+ }
168
+ if (args.entity) {
169
+ // Return specific entity schema
170
+ const entityName = args.entity.toLowerCase();
171
+ const schema = ENTITY_SCHEMAS[entityName];
172
+ if (!schema) {
173
+ print(success({
174
+ error: `Unknown entity: ${args.entity}`,
175
+ availableEntities: Object.keys(ENTITY_SCHEMAS),
176
+ }));
177
+ return;
178
+ }
179
+ const result = { ...schema };
180
+ if (!flags['include-examples'] && 'examples' in result) {
181
+ delete result.examples;
182
+ }
183
+ print(success(result));
184
+ return;
185
+ }
186
+ // Return list of available entities with basic info
187
+ const entities = Object.entries(ENTITY_SCHEMAS).map(([name, schema]) => ({
188
+ name,
189
+ description: schema.description,
190
+ operations: schema.operations,
191
+ }));
192
+ print(success({
193
+ entities,
194
+ usage: 'Use "linear schema <entity>" for detailed schema',
195
+ fullSchema: 'Use "linear schema --full" for complete schema',
196
+ }));
197
+ }
198
+ }
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
@@ -0,0 +1,17 @@
1
+ import { LinearClient } from '@linear/sdk';
2
+ /**
3
+ * Get a Linear client instance authenticated with the configured API key.
4
+ * Creates a new instance on each call (CLI commands are ephemeral).
5
+ *
6
+ * @returns A configured LinearClient instance
7
+ * @throws {CliError} When no API key is configured (NOT_AUTHENTICATED)
8
+ */
9
+ export declare const getClient: () => LinearClient;
10
+ /**
11
+ * Create a new Linear client with a specific API key.
12
+ * Useful for testing auth without saving the key.
13
+ *
14
+ * @param apiKey - Linear API key to authenticate with
15
+ * @returns A configured LinearClient instance
16
+ */
17
+ export declare const createClient: (apiKey: string) => LinearClient;
@@ -0,0 +1,23 @@
1
+ import { LinearClient } from '@linear/sdk';
2
+ import { requireApiKey } from './config.js';
3
+ /**
4
+ * Get a Linear client instance authenticated with the configured API key.
5
+ * Creates a new instance on each call (CLI commands are ephemeral).
6
+ *
7
+ * @returns A configured LinearClient instance
8
+ * @throws {CliError} When no API key is configured (NOT_AUTHENTICATED)
9
+ */
10
+ export const getClient = () => {
11
+ const apiKey = requireApiKey();
12
+ return new LinearClient({ apiKey });
13
+ };
14
+ /**
15
+ * Create a new Linear client with a specific API key.
16
+ * Useful for testing auth without saving the key.
17
+ *
18
+ * @param apiKey - Linear API key to authenticate with
19
+ * @returns A configured LinearClient instance
20
+ */
21
+ export const createClient = (apiKey) => {
22
+ return new LinearClient({ apiKey });
23
+ };
@@ -0,0 +1,33 @@
1
+ import type { ConfigFile } from './types.js';
2
+ /**
3
+ * Read the configuration file.
4
+ * @throws {CliError} When config file exists but cannot be parsed.
5
+ */
6
+ export declare const readConfig: () => ConfigFile;
7
+ /**
8
+ * Write the configuration file atomically.
9
+ * Uses a temp file + rename to prevent corruption.
10
+ */
11
+ export declare const writeConfig: (config: ConfigFile) => void;
12
+ /**
13
+ * Get the API key from config or environment.
14
+ * Environment variable LINEAR_API_KEY takes precedence.
15
+ */
16
+ export declare const getApiKey: () => string | undefined;
17
+ /**
18
+ * Get the API key or throw if not configured.
19
+ * @throws {CliError} When no API key is configured (NOT_AUTHENTICATED).
20
+ */
21
+ export declare const requireApiKey: () => string;
22
+ /**
23
+ * Save the API key to config with secure permissions.
24
+ */
25
+ export declare const saveApiKey: (apiKey: string) => void;
26
+ /**
27
+ * Remove the API key from config.
28
+ */
29
+ export declare const removeApiKey: () => void;
30
+ /**
31
+ * Get the config file path (for display purposes).
32
+ */
33
+ export declare const getConfigPath: () => string;