linear-cli-agents 0.5.1 → 0.6.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/README.md CHANGED
@@ -269,6 +269,25 @@ linear states list
269
269
  linear states list --team ENG
270
270
  ```
271
271
 
272
+ ### Cycles (Sprints)
273
+
274
+ ```bash
275
+ # List all cycles
276
+ linear cycles list
277
+ linear cycles list --team ENG
278
+
279
+ # Filter by status
280
+ linear cycles list --active # Currently running
281
+ linear cycles list --upcoming # Future cycles
282
+ linear cycles list --completed # Past cycles
283
+
284
+ # Get current cycle for a team
285
+ linear cycles current --team ENG
286
+
287
+ # Get cycle details
288
+ linear cycles get CYCLE_ID
289
+ ```
290
+
272
291
  ### Users
273
292
 
274
293
  ```bash
@@ -0,0 +1,11 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class CyclesCurrent extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ 'team-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ team: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ };
10
+ run(): Promise<void>;
11
+ }
@@ -0,0 +1,104 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { getClient } from '../../lib/client.js';
3
+ import { success, print, printItem } from '../../lib/output.js';
4
+ import { handleError, CliError, ErrorCodes } from '../../lib/errors.js';
5
+ export default class CyclesCurrent extends Command {
6
+ static description = 'Get the current active cycle for a team';
7
+ static examples = [
8
+ '<%= config.bin %> cycles current --team ENG',
9
+ '<%= config.bin %> cycles current --team-id TEAM_ID',
10
+ '<%= config.bin %> cycles current --team ENG --format table',
11
+ ];
12
+ static flags = {
13
+ format: Flags.string({
14
+ char: 'F',
15
+ description: 'Output format',
16
+ options: ['json', 'table', 'plain'],
17
+ default: 'json',
18
+ }),
19
+ 'team-id': Flags.string({
20
+ description: 'Team ID',
21
+ exclusive: ['team'],
22
+ }),
23
+ team: Flags.string({
24
+ description: 'Team key (e.g., ENG)',
25
+ exclusive: ['team-id'],
26
+ }),
27
+ };
28
+ async run() {
29
+ try {
30
+ const { flags } = await this.parse(CyclesCurrent);
31
+ const format = flags.format;
32
+ const client = getClient();
33
+ if (!flags['team-id'] && !flags.team) {
34
+ throw new CliError(ErrorCodes.MISSING_REQUIRED_FIELD, 'Team is required. Use --team or --team-id');
35
+ }
36
+ // Build filter for active cycle
37
+ const now = new Date();
38
+ const filter = {
39
+ startsAt: { lte: now },
40
+ endsAt: { gte: now },
41
+ };
42
+ if (flags['team-id']) {
43
+ filter.team = { id: { eq: flags['team-id'] } };
44
+ }
45
+ else if (flags.team) {
46
+ filter.team = { key: { eq: flags.team } };
47
+ }
48
+ const cycles = await client.cycles({
49
+ first: 1,
50
+ filter,
51
+ });
52
+ if (cycles.nodes.length === 0) {
53
+ throw new CliError(ErrorCodes.NOT_FOUND, 'No active cycle found for this team');
54
+ }
55
+ const cycle = cycles.nodes[0];
56
+ const [team, issues] = await Promise.all([cycle.team, cycle.issues()]);
57
+ const issuesSummary = {
58
+ total: issues.nodes.length,
59
+ completed: issues.nodes.filter((i) => i.completedAt).length,
60
+ };
61
+ const data = {
62
+ id: cycle.id,
63
+ number: cycle.number,
64
+ name: cycle.name ?? null,
65
+ description: cycle.description ?? null,
66
+ startsAt: cycle.startsAt,
67
+ endsAt: cycle.endsAt,
68
+ progress: cycle.progress,
69
+ team: team
70
+ ? {
71
+ id: team.id,
72
+ key: team.key,
73
+ name: team.name,
74
+ }
75
+ : null,
76
+ issues: issuesSummary,
77
+ daysRemaining: Math.ceil((new Date(cycle.endsAt).getTime() - now.getTime()) / (1000 * 60 * 60 * 24)),
78
+ };
79
+ if (format === 'json') {
80
+ print(success(data));
81
+ }
82
+ else if (format === 'table') {
83
+ printItem({
84
+ id: data.id,
85
+ number: data.number,
86
+ name: data.name ?? 'Unnamed',
87
+ team: data.team?.key ?? 'N/A',
88
+ startsAt: data.startsAt,
89
+ endsAt: data.endsAt,
90
+ progress: `${Math.round(data.progress * 100)}%`,
91
+ issues: `${issuesSummary.completed}/${issuesSummary.total} completed`,
92
+ daysRemaining: `${data.daysRemaining} days`,
93
+ }, format);
94
+ }
95
+ else {
96
+ console.log(data.id);
97
+ }
98
+ }
99
+ catch (err) {
100
+ handleError(err);
101
+ this.exit(1);
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,12 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class CyclesGet 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
+ format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,86 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { getClient } from '../../lib/client.js';
3
+ import { success, print, printItem } from '../../lib/output.js';
4
+ import { handleError, CliError, ErrorCodes } from '../../lib/errors.js';
5
+ export default class CyclesGet extends Command {
6
+ static description = 'Get cycle (sprint) details';
7
+ static examples = [
8
+ '<%= config.bin %> cycles get CYCLE_ID',
9
+ '<%= config.bin %> cycles get CYCLE_ID --format table',
10
+ ];
11
+ static args = {
12
+ id: Args.string({
13
+ description: 'Cycle ID',
14
+ required: true,
15
+ }),
16
+ };
17
+ static flags = {
18
+ format: Flags.string({
19
+ char: 'F',
20
+ description: 'Output format',
21
+ options: ['json', 'table', 'plain'],
22
+ default: 'json',
23
+ }),
24
+ };
25
+ async run() {
26
+ try {
27
+ const { args, flags } = await this.parse(CyclesGet);
28
+ const format = flags.format;
29
+ const client = getClient();
30
+ const cycle = await client.cycle(args.id);
31
+ if (!cycle) {
32
+ throw new CliError(ErrorCodes.NOT_FOUND, `Cycle ${args.id} not found`);
33
+ }
34
+ const [team, issues] = await Promise.all([cycle.team, cycle.issues()]);
35
+ const issuesSummary = {
36
+ total: issues.nodes.length,
37
+ completed: issues.nodes.filter((i) => i.completedAt).length,
38
+ };
39
+ const data = {
40
+ id: cycle.id,
41
+ number: cycle.number,
42
+ name: cycle.name ?? null,
43
+ description: cycle.description ?? null,
44
+ startsAt: cycle.startsAt,
45
+ endsAt: cycle.endsAt,
46
+ completedAt: cycle.completedAt ?? null,
47
+ progress: cycle.progress,
48
+ scopeHistory: cycle.scopeHistory,
49
+ completedScopeHistory: cycle.completedScopeHistory,
50
+ team: team
51
+ ? {
52
+ id: team.id,
53
+ key: team.key,
54
+ name: team.name,
55
+ }
56
+ : null,
57
+ issues: issuesSummary,
58
+ createdAt: cycle.createdAt,
59
+ updatedAt: cycle.updatedAt,
60
+ };
61
+ if (format === 'json') {
62
+ print(success(data));
63
+ }
64
+ else if (format === 'table') {
65
+ printItem({
66
+ id: data.id,
67
+ number: data.number,
68
+ name: data.name ?? 'Unnamed',
69
+ team: data.team?.key ?? 'N/A',
70
+ startsAt: data.startsAt,
71
+ endsAt: data.endsAt,
72
+ completedAt: data.completedAt ?? 'In progress',
73
+ progress: `${Math.round(data.progress * 100)}%`,
74
+ issues: `${issuesSummary.completed}/${issuesSummary.total} completed`,
75
+ }, format);
76
+ }
77
+ else {
78
+ console.log(data.id);
79
+ }
80
+ }
81
+ catch (err) {
82
+ handleError(err);
83
+ this.exit(1);
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,16 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class CyclesList extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ 'team-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ team: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ active: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ upcoming: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ completed: import("@oclif/core/interfaces").BooleanFlag<boolean>;
12
+ first: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
13
+ after: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ };
15
+ run(): Promise<void>;
16
+ }
@@ -0,0 +1,147 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { getClient } from '../../lib/client.js';
3
+ import { successList, print, printList } from '../../lib/output.js';
4
+ import { handleError } from '../../lib/errors.js';
5
+ import { colors, truncate, formatProgress } from '../../lib/formatter.js';
6
+ const COLUMNS = [
7
+ {
8
+ key: 'number',
9
+ header: '#',
10
+ format: (value) => colors.dim(String(value)),
11
+ },
12
+ {
13
+ key: 'name',
14
+ header: 'NAME',
15
+ format: (value) => (value ? colors.bold(truncate(String(value), 25)) : colors.gray('Unnamed')),
16
+ },
17
+ {
18
+ key: 'teamKey',
19
+ header: 'TEAM',
20
+ format: (value) => colors.cyan(String(value)),
21
+ },
22
+ {
23
+ key: 'startsAt',
24
+ header: 'START',
25
+ format: (value) => colors.dim(new Date(value).toISOString().split('T')[0]),
26
+ },
27
+ {
28
+ key: 'endsAt',
29
+ header: 'END',
30
+ format: (value) => colors.dim(new Date(value).toISOString().split('T')[0]),
31
+ },
32
+ {
33
+ key: 'progress',
34
+ header: 'PROGRESS',
35
+ format: (value) => formatProgress(Number(value)),
36
+ },
37
+ ];
38
+ export default class CyclesList extends Command {
39
+ static description = 'List cycles (sprints)';
40
+ static examples = [
41
+ '<%= config.bin %> cycles list',
42
+ '<%= config.bin %> cycles list --team-id TEAM_ID',
43
+ '<%= config.bin %> cycles list --team ENG',
44
+ '<%= config.bin %> cycles list --format table',
45
+ '<%= config.bin %> cycles list --active',
46
+ ];
47
+ static flags = {
48
+ format: Flags.string({
49
+ char: 'F',
50
+ description: 'Output format',
51
+ options: ['json', 'table', 'plain'],
52
+ default: 'json',
53
+ }),
54
+ 'team-id': Flags.string({
55
+ description: 'Filter by team ID',
56
+ }),
57
+ team: Flags.string({
58
+ description: 'Filter by team key (e.g., ENG)',
59
+ }),
60
+ active: Flags.boolean({
61
+ description: 'Show only active cycles',
62
+ default: false,
63
+ }),
64
+ upcoming: Flags.boolean({
65
+ description: 'Show only upcoming cycles',
66
+ default: false,
67
+ }),
68
+ completed: Flags.boolean({
69
+ description: 'Show only completed cycles',
70
+ default: false,
71
+ }),
72
+ first: Flags.integer({
73
+ description: 'Number of cycles to fetch (default: 50)',
74
+ default: 50,
75
+ }),
76
+ after: Flags.string({
77
+ description: 'Cursor for pagination',
78
+ }),
79
+ };
80
+ async run() {
81
+ try {
82
+ const { flags } = await this.parse(CyclesList);
83
+ const format = flags.format;
84
+ const client = getClient();
85
+ // Build filter
86
+ const filter = {};
87
+ if (flags['team-id']) {
88
+ filter.team = { id: { eq: flags['team-id'] } };
89
+ }
90
+ else if (flags.team) {
91
+ filter.team = { key: { eq: flags.team } };
92
+ }
93
+ const now = new Date();
94
+ if (flags.active) {
95
+ filter.startsAt = { lte: now };
96
+ filter.endsAt = { gte: now };
97
+ }
98
+ else if (flags.upcoming) {
99
+ filter.startsAt = { gt: now };
100
+ }
101
+ else if (flags.completed) {
102
+ filter.completedAt = { neq: null };
103
+ }
104
+ const cycles = await client.cycles({
105
+ first: flags.first,
106
+ after: flags.after,
107
+ filter: Object.keys(filter).length > 0 ? filter : undefined,
108
+ });
109
+ const data = await Promise.all(cycles.nodes.map(async (cycle) => {
110
+ const team = await cycle.team;
111
+ return {
112
+ id: cycle.id,
113
+ number: cycle.number,
114
+ name: cycle.name ?? null,
115
+ startsAt: cycle.startsAt,
116
+ endsAt: cycle.endsAt,
117
+ completedAt: cycle.completedAt ?? null,
118
+ progress: cycle.progress,
119
+ teamId: team?.id ?? '',
120
+ teamKey: team?.key ?? '',
121
+ teamName: team?.name ?? '',
122
+ };
123
+ }));
124
+ const pageInfo = {
125
+ hasNextPage: cycles.pageInfo.hasNextPage,
126
+ hasPreviousPage: cycles.pageInfo.hasPreviousPage,
127
+ startCursor: cycles.pageInfo.startCursor,
128
+ endCursor: cycles.pageInfo.endCursor,
129
+ };
130
+ if (format === 'json') {
131
+ print(successList(data, pageInfo));
132
+ }
133
+ else {
134
+ printList(data, format, {
135
+ columns: COLUMNS,
136
+ primaryKey: 'name',
137
+ secondaryKey: 'number',
138
+ pageInfo,
139
+ });
140
+ }
141
+ }
142
+ catch (err) {
143
+ handleError(err);
144
+ this.exit(1);
145
+ }
146
+ }
147
+ }
@@ -435,6 +435,40 @@ const COMMANDS = {
435
435
  flags: { format: { type: 'string', options: ['json', 'table', 'plain'], default: 'json' } },
436
436
  examples: ['linear me'],
437
437
  },
438
+ // Cycles
439
+ 'cycles list': {
440
+ description: 'List cycles (sprints)',
441
+ flags: {
442
+ format: { type: 'string', options: ['json', 'table', 'plain'], default: 'json' },
443
+ 'team-id': { type: 'string', description: 'Filter by team ID' },
444
+ team: { type: 'string', description: 'Filter by team key (e.g., ENG)' },
445
+ active: { type: 'boolean', description: 'Show only active cycles' },
446
+ upcoming: { type: 'boolean', description: 'Show only upcoming cycles' },
447
+ completed: { type: 'boolean', description: 'Show only completed cycles' },
448
+ first: { type: 'number', description: 'Number of results' },
449
+ },
450
+ examples: [
451
+ 'linear cycles list',
452
+ 'linear cycles list --team ENG',
453
+ 'linear cycles list --active',
454
+ ],
455
+ },
456
+ 'cycles get': {
457
+ description: 'Get cycle (sprint) details',
458
+ args: { id: { description: 'Cycle ID', required: true } },
459
+ flags: { format: { type: 'string', options: ['json', 'table', 'plain'], default: 'json' } },
460
+ examples: ['linear cycles get CYCLE_ID'],
461
+ },
462
+ 'cycles current': {
463
+ description: 'Get the current active cycle for a team',
464
+ flags: {
465
+ format: { type: 'string', options: ['json', 'table', 'plain'], default: 'json' },
466
+ 'team-id': { type: 'string', description: 'Team ID' },
467
+ team: { type: 'string', description: 'Team key (e.g., ENG)' },
468
+ },
469
+ examples: ['linear cycles current --team ENG'],
470
+ },
471
+ // Other
438
472
  search: {
439
473
  description: 'Search for issues',
440
474
  args: { query: { description: 'Search query', required: true } },
@@ -516,6 +550,20 @@ const ENTITY_SCHEMAS = {
516
550
  teams: 'Associated teams',
517
551
  },
518
552
  },
553
+ cycles: {
554
+ entity: 'cycles',
555
+ operations: ['list', 'get', 'current'],
556
+ description: 'Time-boxed iterations (sprints)',
557
+ fields: {
558
+ id: 'Unique identifier',
559
+ number: 'Cycle number',
560
+ name: 'Cycle name (optional)',
561
+ startsAt: 'Start date',
562
+ endsAt: 'End date',
563
+ progress: 'Completion progress (0-1)',
564
+ team: 'Associated team',
565
+ },
566
+ },
519
567
  teams: {
520
568
  entity: 'teams',
521
569
  operations: ['list'],
@@ -644,7 +692,7 @@ export default class Info extends Command {
644
692
  return acc;
645
693
  }, {});
646
694
  print(success({
647
- version: '0.5.1',
695
+ version: '0.6.0',
648
696
  commands: compactCommands,
649
697
  configKeys: CONFIG_KEYS,
650
698
  note: 'Use "linear info" for full documentation with examples and workflows',
@@ -653,7 +701,7 @@ export default class Info extends Command {
653
701
  }
654
702
  // Full documentation
655
703
  print(success({
656
- version: '0.5.1',
704
+ version: '0.6.0',
657
705
  overview: {
658
706
  description: 'CLI for interacting with Linear, designed for LLMs and agents',
659
707
  authentication: 'Run "linear auth login" or set LINEAR_API_KEY environment variable',
@@ -49,6 +49,10 @@ export declare const formatPriority: (priority: number) => string;
49
49
  * Truncate string to max length with ellipsis.
50
50
  */
51
51
  export declare const truncate: (str: string | undefined | null, maxLength: number) => string;
52
+ /**
53
+ * Format progress as a percentage with color.
54
+ */
55
+ export declare const formatProgress: (progress: number) => string;
52
56
  /**
53
57
  * Generic formatter that outputs data in the specified format.
54
58
  */
@@ -166,6 +166,21 @@ export const truncate = (str, maxLength) => {
166
166
  return str;
167
167
  return str.slice(0, maxLength - 1) + '\u2026';
168
168
  };
169
+ /**
170
+ * Format progress as a percentage with color.
171
+ */
172
+ export const formatProgress = (progress) => {
173
+ const percent = Math.round(progress * 100);
174
+ if (percent >= 100)
175
+ return colors.green(`${percent}%`);
176
+ if (percent >= 75)
177
+ return colors.cyan(`${percent}%`);
178
+ if (percent >= 50)
179
+ return colors.blue(`${percent}%`);
180
+ if (percent >= 25)
181
+ return colors.yellow(`${percent}%`);
182
+ return colors.gray(`${percent}%`);
183
+ };
169
184
  /**
170
185
  * Generic formatter that outputs data in the specified format.
171
186
  */