## Summary
- include resolved-thread metadata and derived `threadId` in `linear
issue view --json`
- hide resolved threads by default in human-readable `issue view`, with
`--show-resolved-threads` to reveal them
- show root thread ids in rendered output and use OSC-8 hyperlinks for
them when terminal hyperlinks are enabled
## Testing
- deno check src/main.ts
- deno lint
- deno task test
- manual QA against CLI-team issues for default view,
`--show-resolved-threads`, JSON output, no-comments JSON, open-only
threads, OSC-8 behavior, and reply creation from printed thread ids
Co-authored-by: Peter Schilling <code@schpet.com>
## Summary
- Add **assignee** and **priority** fields to the `issue view` command's
metadata line
- Priority uses the existing `getPriorityDisplay` function (same visual
format as `issue list`)
- Assignee shows `@displayName` or `Unassigned` when null
- Both GraphQL queries (`GetIssueDetailsWithComments` and
`GetIssueDetails`) updated with `assignee { name displayName }` and
`priority`
## Motivation
These are critical fields for triaging issues. Previously, `issue view`
showed project, milestone, and cycle but not who owns the issue or how
urgent it is.
## Example output
```
# ENG-123: Fix authentication bug
**Priority:** ▄▆█ | **Assignee:** @Jane Smith | **Project:** Platform Infrastructure Q1
```
## Test plan
- [x] Updated all 10 issue-view snapshot tests with mock data covering:
unassigned/assigned, all priority levels (0-4)
- [x] `deno check src/main.ts` passes
- [x] `deno lint` passes
- [x] All issue-view tests pass (`deno test --allow-all --filter "Issue
View"`)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
Adds `-l, --label <label>` flag to `issue list` for filtering issues by
label name. The flag is repeatable — when multiple labels are specified,
only issues matching **all** labels are returned.
- Uses `eqIgnoreCase` for case-insensitive label name matching
- Single label: `filter.labels = { some: { name: { eqIgnoreCase: "Bug" }
} }`
- Multiple labels: `filter.labels = { and: [{ some: { name: {
eqIgnoreCase: "Bug" } } }, ...] }`
### Usage
```bash
# Single label
linear issue list --label Bug
# Multiple labels (AND logic)
linear issue list --label Bug --label "High Priority"
# Combined with other filters
linear issue list --label Bug --state started --cycle active
```
## Test plan
- [x] Added snapshot test for `issue list --label Bug` with mock server
- [x] Updated help text snapshot with new `--label` flag
- [x] `deno fmt` clean
- [x] `deno lint` clean
- [x] All tests pass, existing tests unaffected
---------
Co-authored-by: Peter Schilling <code@schpet.com>
Co-authored-by: Mihai Chiorean <mihai-chiorean@users.noreply.github.com>
## Summary
Adds `--json` / `-j` flag to three commands that were missing structured
output support:
- **`issue list --json`** — outputs issues as a JSON array with `id`,
`identifier`, `title`, `priority`, `estimate`, `state`, `labels`,
`assignee`, and `updatedAt`
- **`issue create --json`** — outputs the created issue's `id`,
`identifier`, `url`, and `team` as JSON (non-interactive path only)
- **`cycle list --json`** — outputs cycles as a JSON array with `id`,
`number`, `name`, `startsAt`, `endsAt`, `completedAt`, `isActive`,
`isFuture`, `isPast`, and `status`
When `--json` is set, the spinner is suppressed and empty results output
`[]` instead of human-readable messages.
This is particularly useful for AI agents and automation that need to
parse CLI output programmatically. The `linear api` raw GraphQL command
works as a workaround today, but first-class `--json` support on
list/create commands is much more ergonomic.
Related: #127 (requests `--json` on project commands — this PR covers
issue and cycle commands using the same pattern)
## Test plan
- [x] Added snapshot test for `issue list --json` with mock server
- [x] Added snapshot test for `issue create --json` with mock server
- [x] Added snapshot test for `cycle list --json` with mock server
- [x] Updated help text snapshots for all three commands
- [x] `deno fmt` clean
- [x] `deno lint` clean
- [x] All new tests pass, existing tests unaffected
Filter issues by project label name, showing issues from all projects
that have a given label. Uses Linear's native GraphQL project label
filtering rather than a two-step query.
A few practical improvements based on real usage friction:
- **Prerequisites**: Added `bunx linear` as a zero-install fallback for
when `linear` isn't on PATH
- **Discovering Options**: Added a note about commands with non-obvious
required flags — specifically that `issue list` requires `--sort` (valid
values: `manual`, `priority`) and `--team`, and that `--no-pager` is
only supported on `issue list`
Show the issue state (e.g., Todo, In Progress, Done) in the metadata
line of `linear issue view`, positioned before Project/Milestone/Cycle.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removes version strings from all generated documentation to avoid
unnecessary churn in PRs when cutting a release. Adds a stripVersion
helper that filters "Version: X.Y.Z" lines from CLI help output, and
removes the "Generated from linear CLI vX.Y.Z" line from the template
and index.
Move API key storage from plaintext TOML to OS-native keyrings (macOS
Keychain, Linux `libsecret`, Windows Credential Manager). The
credentials file retains only workspace metadata. Keys are loaded into
an in-memory cache at startup so all downstream reads remain synchronous
— no changes needed to any command files.
## Changes
### Keyring (`src/keyring/`)
- Platform-detecting wrapper with `getPassword`, `setPassword`,
`deletePassword` exports
- macOS: `/usr/bin/security` (exit 44 = not found)
- Linux: `secret-tool` via stdin for writes (exit 1 = not found)
- Windows: `Deno.dlopen("advapi32.dll")` FFI calling
`CredReadW`/`CredWriteW`/`CredDeleteW` directly
- `_setBackend()` test seam for injecting an in-memory `Map` backend
### Windows Credential Manager via FFI
The Windows backend calls `advapi32.dll` directly via Deno's FFI
(`Deno.dlopen`) rather than shelling out to PowerShell. This matches the
standard approach taken by every comparable credential tool:
-
[`danieljoos/wincred`](https://github.com/danieljoos/wincred/blob/623325312d3224d48d131159187b93e906216563/sys.go)
— Go library calling `advapi32.dll` via `windows.NewLazySystemDLL`, used
by:
-
[`docker-credential-helpers`](https://github.com/docker/docker-credential-helpers/blob/2b4e08bca3dbdb8e6c6e28790042742d0c0fc48f/wincred/wincred.go)
- [`gh`
CLI](https://github.com/cli/cli/blob/2c54a0d36a2f3c9c1f1b869a64120837c3a1e6f5/internal/keyring/keyring.go)
(via
[`zalando/go-keyring`](https://github.com/zalando/go-keyring/blob/5c6f7e0ba54d20daa8ea4e03f7ce0a27c075bfb6/keyring_windows.go))
-
[`aws-vault`](https://github.com/99designs/aws-vault/blob/70522e8f0b8f9c5b4e2e4e1e1e1cc4e3e5c3f04c/go.mod)
(via `99designs/keyring`)
-
[`node-keytar`](https://github.com/atom/node-keytar/blob/deae59a488789f2cd4a8dba6c7e58665795804fe/src/keytar_win.cc)
— C++ N-API addon, `#include <wincred.h>`
-
[`jaraco/keyring`](https://github.com/jaraco/keyring/blob/38c040133559682902f25fe96496756ee6849820/keyring/backends/Windows.py)
— Python, `win32cred` (pywin32-ctypes wrapping advapi32 via ctypes)
The implementation packs the 80-byte `CREDENTIALW` struct manually via
`DataView`, encodes strings as UTF-16LE for the `W`-suffix APIs, and
uses `GetLastError` from `kernel32.dll` to distinguish "not found"
(`ERROR_NOT_FOUND` = 1168) from real failures. DLLs are lazy-loaded so
the module import doesn't fail on macOS/Linux.
### Credentials (`src/credentials.ts`)
- `Credentials` interface changed from index signature to `{ default?:
string; workspaces: string[] }`
- `apiKeyCache` `Map` populated at startup, keeping
`getCredentialApiKey()` sync
- `addCredential`/`removeCredential` write to keyring first, only mutate
local state on success
- `parseInlineCredentials` / `parseKeyringCredentials` /
`populateKeyringCache` extracted from `loadCredentials`
- Parallel keyring lookups via `Promise.all`
- Malformed TOML parse errors caught with recovery guidance
- Warnings for: missing keyring entries, dangling default workspace,
inline format detected
### Backward Compatibility
- Inline-format TOML files (keys stored as `workspace = "lin_api_..."`)
are detected by `hasInlineKeys` and served from the file directly
- `addCredential` on an inline-format installation rewrites the file to
keyring format
### Auth List (`src/commands/auth/auth-list.ts`)
- Replaces removed `getAllCredentials()` with `getApiKeyForWorkspace()`
- Distinguishes auth errors (401/403) from network/other failures
instead of labeling everything "invalid credentials"
### CI
- Added `keyring-integration` job on `macos-latest` and `windows-latest`
for real credential round-trip testing
## Testing
- Subprocess isolation via `deno eval` for credential tests (required by
top-level `await loadCredentials()`)
- Mock keyring backend injected via `_setBackend` — covers happy paths,
error propagation, and cache consistency
- Integration test (`test/keyring.integration.test.ts`) exercises the
real macOS Keychain and Windows Credential Manager lifecycle
- Edge cases covered: keyring write/delete failures leave state
unchanged, null keyring returns warn but don't crash, dangling default
dropped on load, inline→keyring format transition on `addCredential`
## References
Closes#130
---------
Co-authored-by: Peter Schilling <code@schpet.com>
Cliffy's Secret.prompt injects tilde (~) characters at the start and
end of pasted text on Windows terminals (bracket paste mode side-effect).
This causes `auth login` to always fail with "Invalid API key" when the
key is pasted interactively.
Trim whitespace and strip non-key characters from both ends of the input.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds --milestone flag to `linear issue list` to filter issues by project
milestone name. Requires --project since milestones belong to projects.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Validation errors thrown before the `try` block in `project update` were
bypassing `handleError`, resulting in raw stack traces instead of clean
`✗` error messages.
Moves the no-options check, `--start-date` format check, and
`--target-date` format check inside the `try` block so they display
consistently with other error messages.
Found during manual QA of #148.
Co-authored-by: Peter Schilling <code@schpet.com>
- Add 'project update' subcommand with --name, --description, --status,
--lead, --start-date, --target-date, --team options
- Add 'project delete' subcommand with --force to skip confirmation
- Add --json to 'project list' for machine-readable output with UUIDs
- Add --json to 'project create' to return project id/slugId/name/url
Enables automation use cases: create, rename, delete, list project IDs
programmatically without raw GraphQL API calls.
### Summary
- Add `linear cycle list` and `linear cycle view` commands for browsing
team cycles
- Add `--cycle` filter to `linear issue list` for filtering issues by
cycle
- Regenerate skill docs to include new commands
### Problem
PR #150 added cycle support to issue create/update/view, but there was
no way to browse cycles themselves or filter issue lists by cycle. Users
had to know cycle names/numbers without being able to look them up from
the CLI. See #64.
### Fix
Adds a `cycle` command group (aliased `cy`) with two subcommands,
following the same patterns as the existing `milestone` commands:
**`cycle list`** shows all cycles for a team with number, name, dates,
and status. Active cycle is highlighted in green, upcoming cycles use
default color, and completed/past cycles are muted.
```
$ linear cycle list --team XXX
# NAME START END STATUS
3 Sprint 3 2026-03-10 2026-03-24 Upcoming
2 Sprint 2 2026-02-24 2026-03-10 Active
1 Sprint 1 2026-02-10 2026-02-24 Completed
```
**`cycle view`** shows full cycle details including a progress
indicator, description, issue breakdown by state, and first 10 issues.
Accepts cycle name, number, or "active".
```
$ linear cycle view active --team XXX
# Sprint 2
**Number:** 2
**Start:** 2026-02-24
**End:** 2026-03-10
**Status:** Active
**Team:** MyTeam (XXX)
## Issues
**Progress:** 17/50 (34%)
**Total Issues:** 50
**Completed:** 17
**In Progress:** 12
**To Do:** 21
**Issues:**
- XXX-412: Fix auth token refresh (In Progress)
- XXX-398: Add dark mode toggle (Todo)
...
```
**`issue list --cycle`** filters the issue list to a specific cycle,
reusing the existing `getCycleIdByNameOrNumber()` utility and the
GraphQL `IssueFilter.cycle` field.
```
$ linear issue list --cycle active --team XXX --sort priority
◌ ID TITLE LABELS E STATE UPDATED
--- XXX-101 Some task Backend, Feature - Todo 1 day ago
--- XXX-102 Another task Feature - Todo 1 day ago
```
### Why
Cycles are a core part of the Linear workflow and the CLI should support
browsing them. `cycle list` lets you see what's available, `cycle view`
gives you sprint progress at a glance, and `--cycle` on issue list lets
you scope your view to a specific sprint.
### Test Plan
- Tested all three commands against a real Linear workspace with 11
cycles and 50+ issues
- `cycle view` tested with active, by number, and by exact name lookup
- All 220 unit/snapshot tests pass
Adds `linear issue comment delete <commentId>` to delete comments via
the commentDelete GraphQL mutation. Includes snapshot test and updated
skill documentation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add snapshot test for 'issue update -p <priority>' to verify -p maps
to --priority (not --parent), mirroring the fix tested in issue create
- Regenerate skill docs to reflect corrected flag assignments:
--parent (no short flag) and -p, --priority
When the --project flag value doesn't match any project by exact name
(e.g. because the name contains special characters like quotes or
parentheses), try matching by slugId before returning undefined.
This lets users pass either the full project name or the 12-char hex
slug ID (visible in `project list` output and Linear URLs) as the
--project value.
Fixes#157
Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019ca8f4-4000-732f-91e2-5ad51d27008c
When using 'relation add X blocked-by Y', the API is called with swapped
IDs (Y blocks X), but the success message was showing the API's returned
identifiers (in the swapped order) instead of the user-specified order.
Fixes: the message now uses the original issueIdentifier and
relatedIssueIdentifier variables, so 'relation add ENG-123 blocked-by ENG-456'
correctly shows '✓ Created relation: ENG-123 blocked-by ENG-456'.
Closes#152
Add --cycle flag to issue create and update commands, accepting cycle
name, number, or 'active' keyword. Display cycle in issue view output
alongside project and milestone metadata.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
- Adds `--milestone` flag to `issue create` and `issue update` commands
- Resolves milestones by name (case-insensitive) within the specified
project
- For `issue update`, automatically falls back to the issue's existing
project if `--project` is not explicitly provided
- Validates that a project context exists before attempting milestone
lookup, with helpful error messages
- Shows **project** and **milestone** in `issue view` output (displayed
below the title when present)
## Example usage
```sh
# Create an issue with a milestone
linear issue create --title "Implement feature" --team ENG --project "My Project" --milestone "Phase 1"
# Update an issue's milestone (project inferred from issue)
linear issue update ENG-123 --milestone "Phase 2"
# View an issue — now shows project and milestone
linear issue view ENG-123
# => Project: My Project | Milestone: Phase 1
```
## Test plan
- [x] Happy path tests for create and update with milestone
- [x] Snapshot test for `issue view` with project and milestone
displayed
- [x] All 205 tests pass
- [x] `deno check` and `deno lint` pass
- [x] GraphQL codegen regenerated
- [x] Verified end-to-end against Linear API
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>