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,97 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync } from 'node:fs';
4
+ import { CliError, ErrorCodes } from './errors.js';
5
+ const CONFIG_DIR = join(homedir(), '.linear-cli');
6
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
7
+ /**
8
+ * Ensure the config directory exists.
9
+ */
10
+ const ensureConfigDir = () => {
11
+ if (!existsSync(CONFIG_DIR)) {
12
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
13
+ }
14
+ };
15
+ /**
16
+ * Read the configuration file.
17
+ * @throws {CliError} When config file exists but cannot be parsed.
18
+ */
19
+ export const readConfig = () => {
20
+ if (!existsSync(CONFIG_FILE)) {
21
+ return {};
22
+ }
23
+ try {
24
+ const content = readFileSync(CONFIG_FILE, 'utf-8');
25
+ return JSON.parse(content);
26
+ }
27
+ catch (error) {
28
+ if (error instanceof SyntaxError) {
29
+ throw new CliError(ErrorCodes.CONFIG_ERROR, `Failed to parse config file at ${CONFIG_FILE}. Please check the file format or delete it.`, { path: CONFIG_FILE });
30
+ }
31
+ throw error;
32
+ }
33
+ };
34
+ /**
35
+ * Write the configuration file atomically.
36
+ * Uses a temp file + rename to prevent corruption.
37
+ */
38
+ export const writeConfig = (config) => {
39
+ ensureConfigDir();
40
+ const tempFile = join(CONFIG_DIR, `config.json.tmp.${Date.now()}`);
41
+ try {
42
+ writeFileSync(tempFile, JSON.stringify(config, null, 2), { mode: 0o600 });
43
+ renameSync(tempFile, CONFIG_FILE);
44
+ }
45
+ catch (error) {
46
+ try {
47
+ unlinkSync(tempFile);
48
+ }
49
+ catch {
50
+ // Ignore cleanup errors
51
+ }
52
+ throw new CliError(ErrorCodes.CONFIG_ERROR, `Failed to write config: ${error instanceof Error ? error.message : 'Unknown error'}`);
53
+ }
54
+ };
55
+ /**
56
+ * Get the API key from config or environment.
57
+ * Environment variable LINEAR_API_KEY takes precedence.
58
+ */
59
+ export const getApiKey = () => {
60
+ const envKey = process.env.LINEAR_API_KEY;
61
+ if (envKey) {
62
+ return envKey;
63
+ }
64
+ const config = readConfig();
65
+ return config.apiKey;
66
+ };
67
+ /**
68
+ * Get the API key or throw if not configured.
69
+ * @throws {CliError} When no API key is configured (NOT_AUTHENTICATED).
70
+ */
71
+ export const requireApiKey = () => {
72
+ const apiKey = getApiKey();
73
+ if (!apiKey) {
74
+ throw new CliError(ErrorCodes.NOT_AUTHENTICATED, 'Not authenticated. Run "linear auth login" or set LINEAR_API_KEY environment variable.');
75
+ }
76
+ return apiKey;
77
+ };
78
+ /**
79
+ * Save the API key to config with secure permissions.
80
+ */
81
+ export const saveApiKey = (apiKey) => {
82
+ const config = readConfig();
83
+ config.apiKey = apiKey;
84
+ writeConfig(config);
85
+ };
86
+ /**
87
+ * Remove the API key from config.
88
+ */
89
+ export const removeApiKey = () => {
90
+ const config = readConfig();
91
+ delete config.apiKey;
92
+ writeConfig(config);
93
+ };
94
+ /**
95
+ * Get the config file path (for display purposes).
96
+ */
97
+ export const getConfigPath = () => CONFIG_FILE;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Standard error codes for the CLI.
3
+ */
4
+ export declare const ErrorCodes: {
5
+ readonly NOT_AUTHENTICATED: "NOT_AUTHENTICATED";
6
+ readonly INVALID_API_KEY: "INVALID_API_KEY";
7
+ readonly NOT_FOUND: "NOT_FOUND";
8
+ readonly ALREADY_EXISTS: "ALREADY_EXISTS";
9
+ readonly INVALID_INPUT: "INVALID_INPUT";
10
+ readonly MISSING_REQUIRED_FIELD: "MISSING_REQUIRED_FIELD";
11
+ readonly API_ERROR: "API_ERROR";
12
+ readonly RATE_LIMITED: "RATE_LIMITED";
13
+ readonly CONFIG_ERROR: "CONFIG_ERROR";
14
+ readonly UNKNOWN_ERROR: "UNKNOWN_ERROR";
15
+ };
16
+ export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
17
+ /**
18
+ * CLI-specific error class that produces structured JSON output.
19
+ */
20
+ export declare class CliError extends Error {
21
+ readonly code: ErrorCode;
22
+ readonly details?: Record<string, unknown> | undefined;
23
+ constructor(code: ErrorCode, message: string, details?: Record<string, unknown> | undefined);
24
+ toResponse(): import("./types.js").ErrorResponse;
25
+ print(): void;
26
+ }
27
+ /**
28
+ * Handle any error and convert to structured output.
29
+ */
30
+ export declare const handleError: (err: unknown) => void;
@@ -0,0 +1,79 @@
1
+ import { error, print } from './output.js';
2
+ /**
3
+ * Standard error codes for the CLI.
4
+ */
5
+ export const ErrorCodes = {
6
+ // Auth errors
7
+ NOT_AUTHENTICATED: 'NOT_AUTHENTICATED',
8
+ INVALID_API_KEY: 'INVALID_API_KEY',
9
+ // Resource errors
10
+ NOT_FOUND: 'NOT_FOUND',
11
+ ALREADY_EXISTS: 'ALREADY_EXISTS',
12
+ // Validation errors
13
+ INVALID_INPUT: 'INVALID_INPUT',
14
+ MISSING_REQUIRED_FIELD: 'MISSING_REQUIRED_FIELD',
15
+ // API errors
16
+ API_ERROR: 'API_ERROR',
17
+ RATE_LIMITED: 'RATE_LIMITED',
18
+ // System errors
19
+ CONFIG_ERROR: 'CONFIG_ERROR',
20
+ UNKNOWN_ERROR: 'UNKNOWN_ERROR',
21
+ };
22
+ /**
23
+ * CLI-specific error class that produces structured JSON output.
24
+ */
25
+ export class CliError extends Error {
26
+ code;
27
+ details;
28
+ constructor(code, message, details) {
29
+ super(message);
30
+ this.code = code;
31
+ this.details = details;
32
+ this.name = 'CliError';
33
+ }
34
+ toResponse() {
35
+ return error(this.code, this.message, this.details);
36
+ }
37
+ print() {
38
+ print(this.toResponse());
39
+ }
40
+ }
41
+ /**
42
+ * Map error message patterns to structured error responses.
43
+ */
44
+ const ERROR_PATTERNS = [
45
+ {
46
+ test: (msg) => msg.includes('401') || msg.includes('Unauthorized'),
47
+ code: ErrorCodes.INVALID_API_KEY,
48
+ message: 'Invalid or expired API key',
49
+ },
50
+ {
51
+ test: (msg) => msg.includes('404') || msg.includes('not found'),
52
+ code: ErrorCodes.NOT_FOUND,
53
+ message: null,
54
+ },
55
+ {
56
+ test: (msg) => msg.includes('429') || msg.includes('rate limit'),
57
+ code: ErrorCodes.RATE_LIMITED,
58
+ message: 'Rate limit exceeded. Please wait before retrying.',
59
+ },
60
+ ];
61
+ /**
62
+ * Handle any error and convert to structured output.
63
+ */
64
+ export const handleError = (err) => {
65
+ if (err instanceof CliError) {
66
+ err.print();
67
+ return;
68
+ }
69
+ if (err instanceof Error) {
70
+ const matched = ERROR_PATTERNS.find((pattern) => pattern.test(err.message));
71
+ if (matched) {
72
+ print(error(matched.code, matched.message ?? err.message));
73
+ return;
74
+ }
75
+ print(error(ErrorCodes.API_ERROR, err.message));
76
+ return;
77
+ }
78
+ print(error(ErrorCodes.UNKNOWN_ERROR, 'An unexpected error occurred'));
79
+ };
@@ -0,0 +1,17 @@
1
+ import type { LinearClient } from '@linear/sdk';
2
+ /**
3
+ * Parse an issue identifier (e.g., ENG-123) into team key and number.
4
+ */
5
+ export declare const parseIdentifier: (identifier: string) => {
6
+ teamKey: string;
7
+ number: number;
8
+ } | null;
9
+ /**
10
+ * Check if a string is a UUID (issue ID).
11
+ */
12
+ export declare const isUUID: (str: string) => boolean;
13
+ /**
14
+ * Resolve an issue identifier or ID to an issue ID.
15
+ * Supports both UUIDs and identifiers like ENG-123.
16
+ */
17
+ export declare const resolveIssueId: (client: LinearClient, idOrIdentifier: string) => Promise<string>;
@@ -0,0 +1,56 @@
1
+ import { CliError, ErrorCodes } from './errors.js';
2
+ /**
3
+ * Parse an issue identifier (e.g., ENG-123) into team key and number.
4
+ */
5
+ export const parseIdentifier = (identifier) => {
6
+ const match = identifier.match(/^([A-Za-z]+)-(\d+)$/);
7
+ if (!match) {
8
+ return null;
9
+ }
10
+ return {
11
+ teamKey: match[1].toUpperCase(),
12
+ number: parseInt(match[2], 10),
13
+ };
14
+ };
15
+ /**
16
+ * Check if a string is a UUID (issue ID).
17
+ */
18
+ export const isUUID = (str) => {
19
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str);
20
+ };
21
+ /**
22
+ * Resolve an issue identifier or ID to an issue ID.
23
+ * Supports both UUIDs and identifiers like ENG-123.
24
+ */
25
+ export const resolveIssueId = async (client, idOrIdentifier) => {
26
+ // If it's a UUID, return as-is
27
+ if (isUUID(idOrIdentifier)) {
28
+ return idOrIdentifier;
29
+ }
30
+ // Try to parse as identifier
31
+ const parsed = parseIdentifier(idOrIdentifier);
32
+ if (!parsed) {
33
+ throw new CliError(ErrorCodes.INVALID_INPUT, `Invalid issue ID or identifier: ${idOrIdentifier}. Expected UUID or format like ENG-123.`);
34
+ }
35
+ // Find the team by key
36
+ const teams = await client.teams({
37
+ filter: { key: { eq: parsed.teamKey } },
38
+ first: 1,
39
+ });
40
+ if (teams.nodes.length === 0) {
41
+ throw new CliError(ErrorCodes.NOT_FOUND, `Team with key "${parsed.teamKey}" not found`);
42
+ }
43
+ const team = teams.nodes[0];
44
+ // Find the issue by team and number
45
+ const issues = await client.issues({
46
+ filter: {
47
+ team: { id: { eq: team.id } },
48
+ number: { eq: parsed.number },
49
+ },
50
+ first: 1,
51
+ });
52
+ if (issues.nodes.length === 0) {
53
+ throw new CliError(ErrorCodes.NOT_FOUND, `Issue ${idOrIdentifier} not found`);
54
+ }
55
+ return issues.nodes[0].id;
56
+ };
@@ -0,0 +1,16 @@
1
+ import type { CommandResponse, CommandListResponse, PageInfo, ErrorResponse } from './types.js';
2
+ /**
3
+ * Output utilities for consistent JSON formatting.
4
+ * All CLI output goes through these functions for parseable responses.
5
+ */
6
+ export declare const success: <T>(data: T) => CommandResponse<T>;
7
+ export declare const successList: <T>(data: T[], pageInfo?: PageInfo) => CommandListResponse<T>;
8
+ export declare const error: (code: string, message: string, details?: Record<string, unknown>) => ErrorResponse;
9
+ /**
10
+ * Print a response as formatted JSON to stdout.
11
+ */
12
+ export declare const print: <T>(response: CommandResponse<T> | CommandListResponse<T>) => void;
13
+ /**
14
+ * Print raw data as formatted JSON to stdout.
15
+ */
16
+ export declare const printRaw: (data: unknown) => void;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Output utilities for consistent JSON formatting.
3
+ * All CLI output goes through these functions for parseable responses.
4
+ */
5
+ export const success = (data) => ({
6
+ success: true,
7
+ data,
8
+ });
9
+ export const successList = (data, pageInfo) => ({
10
+ success: true,
11
+ data,
12
+ ...(pageInfo && { pageInfo }),
13
+ });
14
+ export const error = (code, message, details) => ({
15
+ success: false,
16
+ error: {
17
+ code,
18
+ message,
19
+ ...(details && { details }),
20
+ },
21
+ });
22
+ /**
23
+ * Print a response as formatted JSON to stdout.
24
+ */
25
+ export const print = (response) => {
26
+ console.log(JSON.stringify(response, null, 2));
27
+ };
28
+ /**
29
+ * Print raw data as formatted JSON to stdout.
30
+ */
31
+ export const printRaw = (data) => {
32
+ console.log(JSON.stringify(data, null, 2));
33
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Standard output types for all CLI commands.
3
+ * All outputs are JSON-formatted for easy parsing by LLMs.
4
+ */
5
+ export interface PageInfo {
6
+ hasNextPage: boolean;
7
+ hasPreviousPage: boolean;
8
+ startCursor?: string;
9
+ endCursor?: string;
10
+ }
11
+ export interface SuccessResponse<T> {
12
+ success: true;
13
+ data: T;
14
+ }
15
+ export interface SuccessListResponse<T> {
16
+ success: true;
17
+ data: T[];
18
+ pageInfo?: PageInfo;
19
+ }
20
+ export interface ErrorResponse {
21
+ success: false;
22
+ error: {
23
+ code: string;
24
+ message: string;
25
+ details?: Record<string, unknown>;
26
+ };
27
+ }
28
+ export type CommandResponse<T> = SuccessResponse<T> | ErrorResponse;
29
+ export type CommandListResponse<T> = SuccessListResponse<T> | ErrorResponse;
30
+ export interface ConfigFile {
31
+ apiKey?: string;
32
+ defaultTeamId?: string;
33
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Standard output types for all CLI commands.
3
+ * All outputs are JSON-formatted for easy parsing by LLMs.
4
+ */
5
+ export {};