Skip to content

Commit 66b5002

Browse files
committed
Merge upstream release/2025.11.2 and resolve conflicts
- Keep both our fragments (cycles, milestones) and upstream fragments (comments, parent, children) - Rebuild dist files after conflict resolution
2 parents c7a4c8d + 4d9de1e commit 66b5002

29 files changed

+820
-153
lines changed

AGENTS.md

Lines changed: 98 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@ This file provides guidance to LLM agents when working with code in this reposit
44

55
## Project Overview
66

7-
This is a CLI tool for Linear.app that outputs structured JSON data, designed for LLM agents and users who prefer structured output. Written in Typescript, built with Node.js using Commander.js for CLI structure and the Linear GraphQL API.
7+
Linearis is a CLI tool for Linear.app that outputs structured JSON data, designed for LLM agents and users who prefer structured output. Written in TypeScript, built with Node.js using Commander.js for CLI structure and optimized GraphQL queries for Linear API integration.
8+
9+
**Design philosophy:** Minimize token usage for LLM agents while providing rich, structured data. The entire usage guide (`linearis usage`) comes in under 1000 tokens.
810

911
## Key Commands
1012

1113
### Development Commands
1214

13-
- `node src/main.ts` - Run the CLI application
14-
- `pnpm test` - Run tests (currently not implemented)
15+
- `pnpm start` - Run CLI in development mode using tsx (no compilation)
16+
- `pnpm run build` - Compile TypeScript to dist/ and make executable
17+
- `pnpm run clean` - Remove dist/ directory
18+
- `node dist/main.js` - Run compiled production version
19+
- `pnpm test` - Tests (currently not implemented)
1520

1621
### Package Management
1722

@@ -23,53 +28,64 @@ This is a CLI tool for Linear.app that outputs structured JSON data, designed fo
2328

2429
### Core Components
2530

26-
- **src/main.ts** - Main entry point and CLI command structure using Commander.js
27-
- **src/utils/linear-client.ts** - Complete Linear API service layer with smart ID resolution
28-
- **src/utils/auth.ts** - Authentication handling for API tokens
29-
- **src/utils/output.ts** - JSON output utilities and error handling
30-
- **src/commands/issues.ts** - Issues command implementation with full SPEC compliance
31-
- **src/commands/projects.ts** - Projects command implementation
32-
- **src/commands/embeds.ts** - File download command for Linear uploaded files
33-
- **src/utils/embed-parser.ts** - Markdown parsing for Linear upload URL extraction
34-
- **src/utils/file-service.ts** - Authenticated file download service with signed URL support
35-
- **package.json** - Node.js project configuration with pnpm package manager
31+
**Command Layer** (`src/commands/`)
32+
33+
- Each command file exports a `setup*Commands(program)` function
34+
- Commands registered in `src/main.ts` with Commander.js
35+
- All commands use `handleAsyncCommand()` wrapper for consistent error handling
36+
- Current commands: issues, comments, labels, projects, cycles, projectMilestones
37+
38+
**Service Layer** (`src/utils/`)
39+
40+
- `graphql-service.ts` - Raw GraphQL execution and batch operations
41+
- `graphql-issues-service.ts` - Optimized single-query issue operations
42+
- `linear-service.ts` - Smart ID resolution and SDK fallback operations
43+
- `auth.ts` - Multi-source authentication (flag, env var, file)
44+
- `output.ts` - JSON formatting and error handling
45+
46+
**Query Definitions** (`src/queries/`)
47+
48+
- GraphQL query strings using fragments for reusability
49+
- `common.ts` contains shared fragments (COMPLETE_ISSUE_FRAGMENT, etc.)
50+
- Query files organized by entity (issues.ts, cycles.ts, projectMilestones.ts)
51+
52+
**Type System** (`src/utils/linear-types.d.ts`)
53+
54+
- TypeScript interfaces for all Linear entities
55+
- Ensures type safety across service layers
3656

3757
### Authentication Flow
3858

39-
The CLI supports three authentication methods (in order of preference):
59+
Three authentication methods (checked in order):
4060

4161
1. `--api-token` command flag
4262
2. `LINEAR_API_TOKEN` environment variable
4363
3. Plain text file at `$HOME/.linear_api_token`
4464

45-
### Dependencies
65+
### Smart ID Resolution
4666

47-
- **@linear/sdk** (^58.1.0) - Official Linear TypeScript SDK
48-
- **commander** (^14.0.0) - CLI framework for command structure
49-
- **tsx** (^4.20.5) - TypeScript execution for Node.js
67+
Users can provide human-friendly identifiers that get automatically resolved:
5068

51-
### Technical Requirements
69+
- **Issue IDs**: `ABC-123` → UUID (parses team key + issue number)
70+
- **Project names**: `"Mobile App"` → project UUID
71+
- **Label names**: `"Bug", "Enhancement"` → label UUIDs
72+
- **Team identifiers**: `"ABC"` (key) or `"My Team"` (name) → team UUID
73+
- **Cycle names**: `"Sprint 2025-10"` → cycle UUID (with team disambiguation)
5274

53-
- Node.js >= 22.0.0
54-
- Uses ES modules (implied by .ts and modern Node version)
55-
- All CLI output should be JSON format (except usage examples)
75+
All resolution happens in `LinearService` via `resolve*Id()` methods.
5676

57-
### Development Notes
77+
### GraphQL Optimization Pattern
5878

59-
- All TypeScript files are fully implemented with proper typing
60-
- LinearService provides smart ID resolution (ABC-123 → UUID, label names → IDs, etc.)
61-
- Issue creation supports both `--project` and `--project-id` flags as specified
62-
- Smart parameter conversion allows using human-friendly names instead of UUIDs
63-
- No testing framework currently configured
79+
**Problem:** Linear SDK creates N+1 queries when fetching related entities.
6480

65-
### Smart ID Resolution Features
81+
**Solution:** Custom GraphQL queries with fragments fetch everything in one request.
6682

67-
The CLI automatically handles conversions between user-friendly and internal identifiers:
83+
Example - listing issues:
6884

69-
- **Issue IDs**: `ABC-123` ↔ internal UUID
70-
- **Project names**: `"My Project"` → project UUID
71-
- **Label names**: `"Bug", "Enhancement"` → label UUIDs
72-
- **Team keys/names**: `"ABC"` or `"My Team"` → team UUID
85+
- SDK approach: 1 query for issues + 5 queries per issue (team, assignee, state, project, labels) = 1 + (5 × N) queries
86+
- GraphQL approach: 1 query with all relationships embedded = 1 query total
87+
88+
See `src/queries/common.ts` for fragment definitions and `src/utils/graphql-issues-service.ts` for usage.
7389

7490
### File Download Features
7591

@@ -80,3 +96,51 @@ The CLI can extract and download files uploaded to Linear's private cloud storag
8096
- **File Downloads**: `embeds download <url>` command downloads files from signed URLs
8197
- **Expiration Tracking**: Each embed includes `expiresAt` timestamp (ISO 8601) indicating when the signed URL expires
8298
- **Smart Auth**: FileService automatically detects signed URLs and skips Bearer token authentication when signature is present
99+
100+
## Development Patterns
101+
102+
### Adding a New Command
103+
104+
1. Create command file in `src/commands/` (e.g., `milestones.ts`)
105+
2. Export `setup*Commands(program: Command)` function
106+
3. Register in `src/main.ts` by importing and calling setup function
107+
4. Use `handleAsyncCommand()` wrapper for all async actions
108+
5. Create services with `createGraphQLService()` and/or `createLinearService()`
109+
6. Output results with `outputSuccess(data)` or let errors propagate
110+
111+
### Adding GraphQL Queries
112+
113+
1. Define fragments in `src/queries/common.ts` if reusable
114+
2. Create query strings in `src/queries/<entity>.ts`
115+
3. Use fragments to ensure consistent data fetching
116+
4. Add corresponding method in `GraphQLIssuesService` or create new service
117+
5. Test that all nested relationships are fetched in single query
118+
119+
### Error Handling
120+
121+
- All commands wrapped with `handleAsyncCommand()` which catches and formats errors
122+
- Service methods throw descriptive errors: `throw new Error("Team 'ABC' not found")`
123+
- GraphQL errors transformed to match service error patterns in `GraphQLService.rawRequest()`
124+
125+
## Technical Requirements
126+
127+
- Node.js >= 22.0.0
128+
- ES modules (type: "module" in package.json)
129+
- All CLI output must be JSON format (except help/usage text)
130+
- TypeScript with full type safety
131+
132+
## Dependencies
133+
134+
- `@linear/sdk` (^58.1.0) - Official Linear TypeScript SDK and GraphQL client
135+
- `commander` (^14.0.0) - CLI framework
136+
- `tsx` (^4.20.5) - TypeScript execution for development
137+
138+
## Documentation
139+
140+
Comprehensive docs in `docs/`:
141+
142+
- `architecture.md` - Component organization, data flow, optimization patterns
143+
- `development.md` - Code patterns, TypeScript standards, common workflows
144+
- `build-system.md` - TypeScript compilation, automated builds
145+
- `testing.md` - Testing approach, manual validation, performance benchmarks
146+
- `files.md` - Complete file catalog

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22

33
All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
44

5+
## [2025.11.2] - TODO-TO-DO
6+
7+
### Added
8+
9+
- `issues` commands now include parent and child issue relationships
10+
- `parent` field with `{ id, identifier, title }` for parent issue (if exists)
11+
- `children` array with `{ id, identifier, title }` for immediate child issues
12+
- Available in all issue commands: `read`, `list`, and `search`
13+
14+
### Changed
15+
16+
- Under-the-hood stability bug fixes.
17+
518
## [2025.11.1] - 2025-11-06
619

720
### Added
@@ -41,6 +54,7 @@ All notable changes to this project will be documented in this file. The format
4154

4255
- Initial release of Linearis CLI tool
4356

57+
[2025.11.1]: https://github.com/czottmann/linearis/compare/2025.11.1...2025.11.2
4458
[2025.11.1]: https://github.com/czottmann/linearis/compare/1.1.0...2025.11.1
4559
[1.1.0]: https://github.com/czottmann/linearis/compare/1.0.0...1.1.0
4660
[1.0.0]: https://github.com/czottmann/linearis/releases/tag/1.0.0

dist/main.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { outputUsageInfo } from "./utils/usage.js";
1111
program
1212
.name("linearis")
1313
.description("CLI for Linear.app with JSON output")
14-
.version("2025.11.1")
14+
.version("2025.11.2")
1515
.option("--api-token <token>", "Linear API token");
1616
program.action(() => {
1717
program.help();

dist/queries/common.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,22 @@ export const ISSUE_COMMENTS_FRAGMENT = `
6969
}
7070
}
7171
`;
72+
export const ISSUE_PARENT_FRAGMENT = `
73+
parent {
74+
id
75+
identifier
76+
title
77+
}
78+
`;
79+
export const ISSUE_CHILDREN_FRAGMENT = `
80+
children {
81+
nodes {
82+
id
83+
identifier
84+
title
85+
}
86+
}
87+
`;
7288
export const COMPLETE_ISSUE_FRAGMENT = `
7389
${ISSUE_CORE_FIELDS}
7490
${ISSUE_STATE_FRAGMENT}
@@ -78,6 +94,8 @@ export const COMPLETE_ISSUE_FRAGMENT = `
7894
${ISSUE_LABELS_FRAGMENT}
7995
${ISSUE_CYCLE_FRAGMENT}
8096
${ISSUE_PROJECT_MILESTONE_FRAGMENT}
97+
${ISSUE_PARENT_FRAGMENT}
98+
${ISSUE_CHILDREN_FRAGMENT}
8199
`;
82100
export const COMPLETE_ISSUE_WITH_COMMENTS_FRAGMENT = `
83101
${COMPLETE_ISSUE_FRAGMENT}

dist/queries/issues.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export const BATCH_RESOLVE_FOR_UPDATE_QUERY = `
157157
}
158158
}
159159
160-
# Resolve issue by identifier if needed
160+
# Resolve issue identifier if provided
161161
issues(
162162
filter: {
163163
and: [

dist/utils/graphql-issues-service.js

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { BATCH_RESOLVE_FOR_CREATE_QUERY, BATCH_RESOLVE_FOR_SEARCH_QUERY, BATCH_RESOLVE_FOR_UPDATE_QUERY, CREATE_ISSUE_MUTATION, FILTERED_SEARCH_ISSUES_QUERY, GET_ISSUE_BY_ID_QUERY, GET_ISSUE_BY_IDENTIFIER_QUERY, GET_ISSUES_QUERY, SEARCH_ISSUES_QUERY, UPDATE_ISSUE_MUTATION, } from "../queries/issues.js";
22
import { extractEmbeds } from "./embed-parser.js";
33
import { isUuid } from "./uuid.js";
4+
import { parseIssueIdentifier, tryParseIssueIdentifier, } from "./identifier-parser.js";
45
export class GraphQLIssuesService {
56
graphQLService;
67
linearService;
@@ -30,15 +31,7 @@ export class GraphQLIssuesService {
3031
issueData = result.issue;
3132
}
3233
else {
33-
const parts = issueId.split("-");
34-
if (parts.length !== 2) {
35-
throw new Error(`Invalid issue identifier format: "${issueId}". Expected format: TEAM-123`);
36-
}
37-
const teamKey = parts[0];
38-
const issueNumber = parseInt(parts[1]);
39-
if (isNaN(issueNumber)) {
40-
throw new Error(`Invalid issue number in identifier: "${issueId}"`);
41-
}
34+
const { teamKey, issueNumber } = parseIssueIdentifier(issueId);
4235
const result = await this.graphQLService.rawRequest(GET_ISSUE_BY_IDENTIFIER_QUERY, {
4336
teamKey,
4437
number: issueNumber,
@@ -55,15 +48,9 @@ export class GraphQLIssuesService {
5548
let currentIssueLabels = [];
5649
const resolveVariables = {};
5750
if (!isUuid(args.id)) {
58-
const parts = args.id.split("-");
59-
if (parts.length !== 2) {
60-
throw new Error(`Invalid issue identifier format: "${args.id}". Expected format: TEAM-123`);
61-
}
62-
resolveVariables.teamKey = parts[0];
63-
resolveVariables.issueNumber = parseInt(parts[1]);
64-
if (isNaN(resolveVariables.issueNumber)) {
65-
throw new Error(`Invalid issue number in identifier: "${args.id}"`);
66-
}
51+
const { teamKey, issueNumber } = parseIssueIdentifier(args.id);
52+
resolveVariables.teamKey = teamKey;
53+
resolveVariables.issueNumber = issueNumber;
6754
}
6855
if (args.labelIds && Array.isArray(args.labelIds)) {
6956
const labelNames = args.labelIds.filter((id) => !isUuid(id));
@@ -250,14 +237,10 @@ export class GraphQLIssuesService {
250237
}
251238
}
252239
if (args.parentId && !isUuid(args.parentId)) {
253-
const parts = args.parentId.split("-");
254-
if (parts.length === 2) {
255-
const teamKey = parts[0];
256-
const issueNumber = parseInt(parts[1]);
257-
if (!isNaN(issueNumber)) {
258-
resolveVariables.parentTeamKey = teamKey;
259-
resolveVariables.parentIssueNumber = issueNumber;
260-
}
240+
const parentParsed = tryParseIssueIdentifier(args.parentId);
241+
if (parentParsed) {
242+
resolveVariables.parentTeamKey = parentParsed.teamKey;
243+
resolveVariables.parentIssueNumber = parentParsed.issueNumber;
261244
}
262245
}
263246
let resolveResult = {};
@@ -520,6 +503,18 @@ export class GraphQLIssuesService {
520503
id: label.id,
521504
name: label.name,
522505
})),
506+
parent: issue.parent
507+
? {
508+
id: issue.parent.id,
509+
identifier: issue.parent.identifier,
510+
title: issue.parent.title,
511+
}
512+
: undefined,
513+
children: issue.children?.nodes.map((child) => ({
514+
id: child.id,
515+
identifier: child.identifier,
516+
title: child.title,
517+
})) || undefined,
523518
comments: issue.comments?.nodes.map((comment) => ({
524519
id: comment.id,
525520
body: comment.body,

dist/utils/identifier-parser.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export function parseIssueIdentifier(identifier) {
2+
const parts = identifier.split("-");
3+
if (parts.length !== 2) {
4+
throw new Error(`Invalid issue identifier format: "${identifier}". Expected format: TEAM-123`);
5+
}
6+
const teamKey = parts[0];
7+
const issueNumber = parseInt(parts[1]);
8+
if (isNaN(issueNumber)) {
9+
throw new Error(`Invalid issue number in identifier: "${identifier}"`);
10+
}
11+
return { teamKey, issueNumber };
12+
}
13+
export function tryParseIssueIdentifier(identifier) {
14+
try {
15+
return parseIssueIdentifier(identifier);
16+
}
17+
catch {
18+
return null;
19+
}
20+
}

dist/utils/linear-service.js

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { LinearClient } from "@linear/sdk";
22
import { getApiToken } from "./auth.js";
33
import { isUuid } from "./uuid.js";
4+
import { parseIssueIdentifier } from "./identifier-parser.js";
45
const DEFAULT_CYCLE_PAGINATION_LIMIT = 250;
56
function resolveId(input) {
67
if (isUuid(input)) {
@@ -29,15 +30,7 @@ export class LinearService {
2930
if (isUuid(issueId)) {
3031
return issueId;
3132
}
32-
const parts = issueId.split("-");
33-
if (parts.length !== 2) {
34-
throw new Error(`Invalid issue identifier format: "${issueId}". Expected format: TEAM-123`);
35-
}
36-
const teamKey = parts[0];
37-
const issueNumber = parseInt(parts[1]);
38-
if (isNaN(issueNumber)) {
39-
throw new Error(`Invalid issue number in identifier: "${issueId}"`);
40-
}
33+
const { teamKey, issueNumber } = parseIssueIdentifier(issueId);
4134
const issues = await this.client.issues({
4235
filter: {
4336
number: { eq: issueNumber },

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "linearis",
3-
"version": "2025.11.1",
3+
"version": "2025.11.2",
44
"description": "CLI tool for Linear.app with JSON output, smart ID resolution, and optimized GraphQL queries. Designed for LLM agents and humans who prefer structured data.",
55
"main": "dist/main.js",
66
"type": "module",

0 commit comments

Comments
 (0)