492 Commits

Author SHA1 Message Date
Peter Schilling dcbb7ab372 chore: Release linear-cli version 2.5.0 v2.5.0 2026-08-11 14:52:00 -07:00
Peter Schilling 928b1eb65e Support all six document attachment targets in create/update/list/view
Linear attaches every document to exactly one target — project, issue,
initiative, team, cycle, or release — and documentCreate now rejects
targetless documents outright. The CLI only exposed --project/--issue on
create, --project on update, and slug-only --project plus --issue on list.

The reporter asked for the missing create flags; the consistent fix is
wider: a shared attachment-target module (six long-only flags, exactly-one
validation before any network/editor/stdin work, --team + --cycle
collapsing into one team-scoped cycle target like the issue commands)
now backs create, update, and list, so their semantics cannot drift.
Along the way this removes the interactive "workspace document" option
(it always fails server-side now), gives update the missing --issue,
fixes list --project silently matching slug IDs only, resolves list
filters to IDs so bad input errors instead of returning an empty list,
types the ATTACHMENT column (six namespaces make bare names ambiguous),
and shows all six associations in view/list output.

New shared resolvers: resolveInitiativeId (UUID/slug/name) and
resolveReleaseId (UUID/name/version, paginated to exhaustion so ambiguity
detection sees every candidate, erroring on ambiguous matches rather than
picking one silently). teamId/initiativeId/cycleId are [Internal] in
Linear's schema but verified working with a regular API key, as issueId
was before it became public.

Live-QA'd against a real workspace for project/issue/team/cycle/
initiative targets, including re-pointing (the server clears the old
target). Releases require a Business plan and are covered by mocked
tests and the refreshed schema only.

Github-Issue: Fixes #260
Github-Issue-Url: https://github.com/schpet/linear-cli/issues/260
2026-08-11 14:32:51 -07:00
Peter Schilling b296fe9b2c chore: sync Linear GraphQL schema
Refresh the vendored schema from live introspection. Notably picks up
releaseId/cycleId on DocumentCreateInput/DocumentUpdateInput, the
team/cycle/release relations on DocumentFilter, Document.cycle and
Document.release, and ReleaseFilter — prerequisites for document
attachment-target support.
2026-08-11 14:32:51 -07:00
Peter Schilling cf349ba477 chore: Release linear-cli version 2.4.0 v2.4.0 2026-08-05 15:02:39 -07:00
Peter Schilling 5214ca9b80 Add --add-label and --remove-label to issue update
The reporter asked for a way to detach a label from one issue without
deleting it team-wide, believing --label was additive. It actually
replaces the issue's entire label set (IssueUpdateInput.labelIds), so
the gap was wider than reported: adding one label clobbered the rest.
Rather than only the suggested --remove-label, this maps both
--add-label and --remove-label onto the API's addedLabelIds/
removedLabelIds (one atomic mutation, no read-modify-write). --label
keeps its documented replace semantics for existing scripts, with help
text that now says so. Flag names match gh issue edit, the surface
users and agents reach for first.

A --clear-labels flag was considered (an empty label set is currently
inexpressible in one command) and deliberately deferred: adding a flag
later is backwards compatible, removing one is breaking, and nothing
has asked for clear-all yet.

Invalid combinations error before any network call: --label with
incremental flags, the same resolved label ID in both add and remove,
and --team moves combined with incremental flags (label names resolve
against the destination team, which would make source-team labels
silently unresolvable). Live QA confirmed removing an unattached label
is rejected by Linear's API ("Label <id> is not on issue <id>"), not a
silent no-op — surfaced as-is, consistent with the repo's
explicit-input-errors philosophy.

The reporter's alternative ask (label rename) is deferred: it is
team-wide and would not solve the per-issue detach workflow.

Github-Issue: Fixes #258
Github-Issue-Url: https://github.com/schpet/linear-cli/issues/258
2026-08-05 14:59:10 -07:00
Peter Schilling adf7f9cbec chore: Release linear-cli version 2.3.1 v2.3.1 2026-08-04 14:30:18 -07:00
Peter Schilling fe6c8b0616 Fix document list --issue, which never worked
The filter was built as issue: { identifier: { eq: ... } }, but IssueFilter
has no identifier field — the comparator for the human identifier is spelled
id. Linear rejected the variable during coercion, so every invocation of the
flag failed before reaching the resolver, regardless of whether the issue
existed. The flag has been broken since the command was introduced.

Type the filter local as DocumentFilter instead of any, which turns this class
of mistake into a compile error rather than a runtime API rejection; deno check
flags the bad field directly. Building the filter as a single annotated
expression also drops the deno-lint-ignore and keeps it undefined when neither
--project nor --issue is passed, so an unfiltered list still sends no filter.

The previous tests here were removed for rendering relative timestamps, which
are non-deterministic. The regression test instead goes through --json, which
prints raw timestamps, and declares the exact request variables so the mock
only matches the correct filter shape — verified by restoring the pre-fix code
and watching it fail.
2026-08-04 13:38:35 -07:00
Peter Schilling e855b51f9c chore: Release linear-cli version 2.3.0 v2.3.0 2026-07-23 11:10:45 -07:00
Peter Schilling 78f1812493 Make inline image attachments discoverable, eval-validated
Agents asked to put a visible screenshot on an issue reach for
`issue attach`, which uploads the file but creates a sidebar link
attachment that never renders inline — while the success output
("Attachment created") convinces them the image is visible. The working
path, `issue comment add --attach`, has existed since v2.0.0 but nothing
pointed at it: the skill had no image guidance and the flag was buried in
a reference table.

Three coordinated changes, validated as experiment 2 of the skill eval:

- Skill: a Common Tasks recipe for visible images via
  `issue comment add --attach`, with an explicit warning about
  `issue attach`'s sidebar-only behavior.
- CLI: `issue attach` now says it created a sidebar link attachment,
  and for images prints a copy-pasteable hint suggesting
  `issue comment add --attach` (shell-quoted, --public preserved).
  Help descriptions updated on both commands.
- Eval: new frozen image family (trap-phrased development prompt,
  comment-phrased holdout) plus a sidebar-control case graded on
  positionals, with pre-declared outcome rules, CLI/API control split,
  binary-safe fixture checks, and version-matched shim output.

Result (rules frozen before baseline): image-development went 0/3 to 3/3
— every baseline trial fell into the attach trap and wrongly reported
success; every post-change trial routed straight to the recipe. Image
family 3/6 to 6/6 lands in the pre-declared partial-baseline band, so it
is reported as consistent with improvement (exploratory Fisher p = 0.09)
rather than confirmed. Controls held except one known npx-version-check
grader artifact, adjudicated by an Opus gold-label pass (17/18 agreement
with the deterministic grader).
2026-07-23 10:46:18 -07:00
Peter Schilling c0256eae1e Add a Common Tasks section to the linear-cli skill (#207), eval-validated
Adds ~7 copy-pasteable recipes near the top of SKILL.template.md (and the
generated SKILL.md): filtered queries via issue query (with the issue
list/mine alias gotcha spelled out), my-issues, create with
--description-file and --no-interactive, update state/assignee/labels
(noting label replacement semantics), comment from file, view/URL. The
boundary note stays generic so it doesn't teach the eval's control answers.

Post-change eval, same frozen 36-trial protocol as the baseline: 29/30
supported tasks full success, holdout 15/15, controls 6/6 still correctly
choosing linear api — no overcorrection from the new recipes. The single
failure was a subject first trying the skill's documented npx alternative
(npx @schpet/linear-cli issue create ...), which the frozen grader counts
as a bypass; it was not a GraphQL fallback and the task then completed
correctly via the CLI. With the baseline already at 30/30, the eval finds
no measurable routing headroom at this configuration; the change is
validated as non-regressing rather than as an improvement. See
evals/linear-cli-skill/results/comparison.md.

Related to #207
2026-07-23 10:46:18 -07:00
Peter Schilling 07e7d4b54a Add codex-based eval harness for the linear-cli skill, with baseline
Issue #207 claims agents reading the skill skip dedicated CLI subcommands
and reach for raw GraphQL via `linear api`. Before changing the skill text,
this adds an eval that can actually measure that: codex exec runs each task
prompt in a fully isolated environment (fresh CODEX_HOME + fake HOME so the
globally installed skill can't leak in, recording shims for linear/curl/
npx/npm, workspace-write sandbox) and a deterministic grader classifies
route choice and flag correctness from the recorded invocations.

Cases: five recipe families with development + holdout prompts, plus two
controls where GraphQL is genuinely the right route. Outcome rules were
declared before the baseline ran (see evals/linear-cli-skill/README.md).

Baseline result, 36 trials at low effort on gpt-5.6-sol: 30/30 supported
tasks full success, 6/6 controls on linear api. The premise of #207 did not
reproduce in this configuration — with the skill actually read, routing is
already perfect. Two earlier baseline runs were voided during harness
development because stateless canned outputs (issue view contradicting the
subject's own update; ENG-prefixed identifiers for OPS-team requests)
baited subjects into GraphQL investigation and contaminated the signal;
the shim is now consistency-aware.

Related to #207
2026-07-23 10:46:18 -07:00
Peter Schilling eb6f07499a Improve issue mine's no-team error and suggest linear config in repos
When no team can be determined, issue mine (and its list alias) now uses
the same message as issue query — 'No default team configured and no team
scope provided' — instead of the inaccurate 'Could not determine team key
from directory name or team flag' (the team only ever comes from the
--team flag, LINEAR_TEAM_ID, or team_id config; directory names are not
consulted). The error now carries a suggestion: always offer --team
<key>, and when run inside a git work tree, also point at linear config,
which links the repository to a team by generating .linear.toml.

Repo detection is a new best-effort isInsideGitRepo() helper: any git
failure (not a repo, dubious ownership) counts as false so the optional
hint can never turn the team error into a git crash. The same stale
message remains in cycle-list, cycle-view, team-states, and team-members;
those are deferred so they can adopt the helper in a follow-up.
2026-07-23 09:26:34 -07:00
Peter Schilling 97d6077455 Follow-up to #253: error on invalid configured sort instead of defaulting
Add resolveIssueSort() so all issue-listing commands share one sort
resolution path: --sort flag > LINEAR_ISSUE_SORT env > issue_sort config
> priority default. Unlike the getOption fallback, an explicitly
configured but invalid sort value now errors with guidance instead of
silently sorting by priority; this also fixes the same latent silent
downgrade in issue query.

Also cover the gaps around the new default: a genuinely unconfigured
subprocess test (the repo's own .linear.toml supplies issue_sort when
tests run in-process, so the previous no-config tests were passing via
that config file), a test that a configured sort order still wins over
the default, and updated skill docs that no longer claim issue list
requires a sort order.
2026-07-23 08:37:46 -07:00
Frieder Bluemle b4013c9d8f Default sort to priority
Previously, commands listing issues required a sort order via the
--sort flag, the configuration file, or the LINEAR_ISSUE_SORT
environment variable, and errored out when none was provided. Fall
back to priority sort instead so the commands work out of the box,
and document the default in the --sort help text.

Also ignore the .idea/ directory.
2026-07-23 08:33:15 -07:00
Peter Schilling 487d682f7b chore: Release linear-cli version 2.2.0 v2.2.0 2026-07-22 15:22:26 -07:00
Peter Schilling aad5f2f32a Expose cycles in issue query, mine, and view
Issue tables gain a compact CYC column (only when a listed team has
cycles enabled): 'now' for the active cycle, +1/-1 from the API's
next/previous flags, +N/-N anchored on team.activeCycle, and #N when no
anchor exists (cooldown or pre-first-cycle). issue view annotates its
Cycle meta part with the same token, and issue JSON output now carries
the cycle flags and team anchor in raw GraphQL shape.

The shared cycle resolver understands now/next/previous and signed
offsets, so --cycle on query/mine/create/update and cycle view all
speak the relative vocabulary. issue update gains --clear-cycle
(explicit cycleId: null, mirroring --unassign). Also fixes issue query
--search silently dropping the --cycle filter.
2026-07-21 13:12:00 -07:00
Peter Schilling 1393c70952 Add user list, team members --json, and member role markers
Three related gaps in member listing:

`team members` had no --json output. It now emits the connection shape
({ nodes, pageInfo }) with GraphQL field names preserved, matching label list
and project list.

There was no way to list everyone in the workspace, only per-team. Adds
`linear user list` (alias `u`) querying viewer.organization.users. This is a
sibling command rather than a `team members --organization` flag: workspace
members are definitionally not team members, and a flag that has to error when
combined with the positional team key is the design saying it's two commands.

Members now show admin, owner, and you markers alongside the existing
inactive/guest/not-assignable ones, via a shared renderer.

Along the way this fixes `team members --all`, which was a no-op. getTeamMembers
never passed includeDisabled, so Linear defaulted it to false and disabled users
were never fetched — the client-side `active` filter was narrowing a set that
could not contain them. The regression test pins includeDisabled in the mock
variables, so it fails loudly if the flag stops reaching the API.

Note: the --all fix could not be exercised against real data; the workspace used
for QA has no disabled users.
2026-07-18 14:22:16 -07:00
Peter Schilling fb8887c11c Add issue update --unassign to clear an issue's assignee
There was no way to unassign an issue. The mutation input was built as a
`Record<string, string | number | string[] | undefined>`, a type that
structurally cannot hold null, so `IssueUpdateInput.assigneeId` could never
be set to null no matter what flags were added.

Swap that hand-rolled Record for the codegen'd `IssueUpdateInput` — matching
what `project update` already does — and add an explicit `--unassign` flag.
Passing both --assignee and --unassign is a ValidationError rather than one
silently winning.

No short alias: `-U, --unassigned` is already bound in three sibling commands
as a read-side filter, and putting a data-clearing mutation one shift-key away
from a harmless filter invites accidents.

Verified against the real API that Linear honors `assigneeId: null` — worth
checking explicitly, since it silently ignores `projectId: null` elsewhere.
2026-07-18 14:07:21 -07:00
Peter Schilling 3c0a3dec20 Add --project to document update to re-point a document
`document create` accepts --project but `document update` did not, so a
document's project attachment was fixed at creation time — changing it required
the web UI. Add --project to update (UUID, slug ID, or name), reusing the same
resolveProjectId path as create so resolution and errors stay consistent.

The reporter framed this around Linear's web UI supporting multiple project
links per document with add/remove semantics (--project X --remove). The API
tells a different story: a Document has a single related project (the scalar
DocumentUpdateInput.projectId), and create requires exactly one anchor — so a
document always has one project and setting merely replaces it. A detach flag
was prototyped, but live testing showed the API silently ignores
`projectId: null` (returns success, keeps the project), so shipping it would
have been a silent no-op; it was dropped rather than lie about detaching.

Github-Issue: Fixes #225
Github-Issue-Url: https://github.com/schpet/linear-cli/issues/225
2026-07-18 12:50:07 -07:00
Peter Schilling b3a41f7d8c Add team states command and list valid states on wrong --state
Discovering a team's valid workflow state names required a raw GraphQL query,
and passing a wrong `--state` to `issue create`/`issue update` failed with a
bare "Workflow state not found" and no hint at the valid options.

Add `linear team states [teamKey]` (table + `--json`) reusing the existing
getWorkflowStates helper, and make the wrong-state failure actionable: both
issue commands now fetch the states once, resolve against them, and on a miss
throw an error that lists the valid states and points at `linear team states`.
Resolution moves to a pure resolveWorkflowState + a shared
workflowStateNotFoundError factory so both call sites stay identical and the
matching logic is unit-testable; the fetched list is reused for the suggestion
(no second round-trip).

The reporter also hypothesized a raw TypeError for unknown teams; verified this
is false — team(id) is non-null in the schema and an unknown team already
yields a clean "Could not find referenced Team." error, so no null guard is
added. The `--state` help text was left unchanged to avoid churning the
(width-sensitive, globally stale) generated skill docs; the enriched error is
the load-bearing discovery path.
2026-07-17 14:06:50 -07:00
Peter Schilling 70976249ad Point config suggestions at the real linear config command
Three user-facing strings advertised a `linear configure` command that does
not exist — the interactive setup command is registered as `config`, so
`linear configure` failed with "Unknown command". The reporter hit this via
`linear team id` with no team configured.

Fix all three suggestions to name the canonical `config` command, and add
`configure` as an alias so the natural command people (and the CLI's own help
text) reach for just works instead of erroring. Keeping `config` canonical in
every string and doc while tolerating `configure` is more robust than a bare
text swap: the alias matches the exact instinct that produced the bug.

Also upgrade the bare `Error` thrown for an integer id with no team to a
`ValidationError` so it renders with the standard ✗ + suggestion treatment.

The root command moves from src/main.ts into src/cli.ts so it can be imported
by tests (to assert the alias resolves) without its complex inferred cliffy
type entering the published public API and tripping no-slow-types; main.ts
stays the entry point and only runs it under import.meta.main.
2026-07-17 11:39:18 -07:00
Peter Schilling f721c86f6e chore: Release linear-cli version 2.1.1 v2.1.1 2026-07-15 16:04:39 -07:00
Prasanth Somasundar 770d5cfecd Fix Linux keyring command handling
Detect secret-tool availability by whether the executable can be launched,
rather than by probing the Secret Service.

Distinguish missing lookup results from operational failures using stderr,
and stop treating clear failures as successful deletions. Extend the
integration test to cover missing D-Bus and missing executable cases.

Fixes schpet/linear-cli#231
2026-07-15 15:58:08 -07:00
Peter Schilling a1d73708df chore: Release linear-cli version 2.1.0 v2.1.0 2026-07-14 22:13:29 -07:00
Peter Schilling 694441c018 docs(justfile): correct the dist install hint for dist-generate
The comment pointed at astral-sh/cargo-dist v0.28.3, but that repo stops at
0.28.7 and the version this workspace pins (cargo-dist-version = 0.31.0) is
only published from axodotdev/cargo-dist, so the documented command fails with
'failed to find tag v0.31.0'.

Point at the version dist-workspace.toml actually pins, and note why matching
it matters: 'dist generate' rewrites cargo-dist-version to whatever version
generated the file, so running a mismatched dist silently repins the workspace.
2026-07-14 22:11:41 -07:00
Peter Schilling 12860b253c ci: pin deno to 2.7.9 in the release and publish pipelines
027f25e pinned deno to 2.7.9 in mise.toml and ci.yaml, but the two workflows
that actually build and ship artifacts were left on 'v2.x': release.yml runs
'deno compile' for every target triple, and publish.yaml runs codegen before
'jsr publish'. So the binaries users install were produced by whatever v2.x
resolved to at build time, not the toolchain the project pins and tests on.

build-setup.yml is the dist 'github-build-setup' hook that generates the deno
step in release.yml, so it and the generated line are updated together to keep
'dist generate' a no-op.

setup-deno@v2 already runs an exact 2.7.9 in both ci.yaml jobs, so this is the
version resolution those jobs have been proving all along.
2026-07-14 22:10:47 -07:00
Peter Schilling d1beb924e4 fix(test): skip keyring integration test when the macOS keychain is locked
isKeyringAvailable() only probed on Linux and returned true unconditionally
everywhere else, so on macOS the keyring round-trip always attempted a real
write. Under ssh or an agent shell the login keychain has no UI session to
unlock it, so 'security add-generic-password' fails with exit 36
(errSecInteractionNotAllowed) and 'deno task test' fails locally even though
nothing is wrong with the code.

Probe with 'security show-keychain-info', which is read-only and returns 36 in
exactly that state. A read-only probe is not enough on its own: reads still
succeed while locked (find-generic-password returns 44), so only a
write-capable check detects it. The guard keys on 36 specifically rather than
any nonzero exit, so a genuine keyring bug still fails the test loudly.

CI is unaffected: its macOS keychain is unlocked, and the keyring job now sets
LINEAR_KEYRING_INTEGRATION=1 to force the test to run, so a probe that ever
wrongly reports 'unavailable' cannot quietly turn that job into a no-op.
2026-07-14 22:10:09 -07:00
Peter Schilling 7e84ad9747 feat(project): add --label to project update
Adds a repeatable `--label` flag to `linear project update`, mirroring the
existing `project create --label`: names resolve case-insensitively and an
unknown label raises NotFoundError (no auto-create).

The flag uses replace semantics — the supplied labels become the project's
complete label set — consistent with `project update --team` and
`issue update --label`. Empty/whitespace labels are rejected up front, and
case-insensitive duplicates collapse to a single ID.

The project-label lookup is extracted from project-create into a shared
getProjectLabelIdByName helper in utils/linear.ts so create and update stay
identical.

This reshapes the update half of #226 to the repo's existing label
conventions; the create half of #226 already shipped in #216, and the PR's
auto-create/interactive-create behavior is intentionally dropped.

Co-authored-by: KinomotoMio <200703522+KinomotoMio@users.noreply.github.com>
2026-07-13 21:37:38 -07:00
Peter Schilling 82daad741e Follow-up to #236: fail hard on fmt errors, trim abort output, render SKILL.md first
Corrective delta on top of #236's generator hardening:

- deno fmt failure is now fatal instead of a logged warning; unformatted
  committed docs would otherwise break `deno fmt --check` in CI.
- The top-level error boundary prints a concise message instead of the raw
  error (and its stack trace) on every abort path.
- SKILL.md is rendered from its template before writeReferences prunes any
  stale docs, so a missing or broken template aborts before touching the
  references directory.
2026-07-13 20:53:47 -07:00
Edwin Hernandez ffb986d3f4 refactor(skill-docs): harden generator and make output deterministic (#236)
This reworks `skills/linear-cli/scripts/generate-docs.ts` for robustness and stable output, and regenerates the references with the new ordering.

Robustness:
- `run()` now catches the error `Deno.Command` throws when a binary is missing (e.g. `NotFound`) instead of crashing, returning a failed result.
- Help-fetch failures are collected and the run aborts before writing, so an error string can never be embedded into committed docs.
- Reference files are written before stale ones are pruned, so a partial failure can no longer gut the `references/` directory.
- `main()` runs under an `import.meta.main` guard with a `.catch` that logs and exits non-zero.
- The two `pop()` non-null assertions are replaced with a safe `lastSegment()` helper.

Output stability:
- Top-level commands and each subcommand list are sorted by name before rendering, so generation is deterministic and stops producing reordering churn between runs (related to the motivation behind dropping the staleness check in #218). This is the source of the large but reorder-only docs diff in this PR. Happy to drop the sort if you'd rather keep CLI help order.

Cleanup:
- The markdown builders are rewritten as pure `map`/`join`/`flatMap` helpers, and the unused `formatCommandMarkdown` helper is removed.

Verified with the repo-pinned Deno (2.7.9): `deno fmt --check`, `deno lint`, `deno check`, and `deno task generate-skill-docs` run twice with an empty diff between runs. The docs diff is reordering only (identical sorted line sets per file).
2026-07-13 20:50:42 -07:00
Ryan Schumacher ad1563877b feat(project): add --description-file and doc 255-char API limit (#227)
The Linear API rejects project descriptions longer than 255 characters,
but the CLI didn't document the limit or offer a way to work around the
shell-quoting friction of long inline strings.

- Add --description-file to project create and project update, mirroring
document create's --content-file. Mutually exclusive with --description.
- Pre-validate description length client-side with a ValidationError
that points users at --description-file or attaching a Document, so they
don't have to wait for the API to reject the request.
- Update --help text on both commands to mention the 255-char limit.

Closes #224

---------

Co-authored-by: Theo Gregory <theo@gregory.sh>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Schilling <code@schpet.com>
Co-authored-by: Bryan <bryandesigning@gmail.com>
Co-authored-by: Joseph <31323641+josephyooo@users.noreply.github.com>
Co-authored-by: Magnus Buvarp <magnus.buvarp@gmail.com>
Co-authored-by: Rengar Lee <Rengarlee@163.com>
Co-authored-by: Hyunsu Lim <hyunsu.lim01@gmail.com>
Co-authored-by: Luc Leray <luc.leray@gmail.com>
Co-authored-by: c-99-e <268417377+c-99-e@users.noreply.github.com>
Co-authored-by: Al Johri <al.johri@gmail.com>
Co-authored-by: Mihai Chiorean <mihai.v.chiorean@gmail.com>
Co-authored-by: Mihai Chiorean <mihai-chiorean@users.noreply.github.com>
Co-authored-by: Jeffrey Holm <jeff.holm@scale.com>
Co-authored-by: Paymahn Moghadasian <paymahn1@gmail.com>
Co-authored-by: Evan Jacobson <evanjacobson3@gmail.com>
Co-authored-by: schpetbot <bot@schpet.com>
Co-authored-by: Alex Broekhof <alex@quilt.com>
2026-07-11 16:14:03 -07:00
Ryan Schumacher dd39d9e269 fix(cli): accept both UUID and name for --project/--milestone (#229)
The CLI was inconsistent about whether --project and --milestone flags
accept a UUID, slug, or name depending on which command you called.
Several commands silently rejected one form with a misleading "not
found" error.

- Extend resolveProjectId to accept UUID, slug, or name uniformly.
- Add resolveMilestoneId that accepts a UUID directly, or a name when
--project is supplied so the milestone lookup can be scoped.
- Wire the resolvers into issue create/update/mine/query, milestone
create/update/list, and milestone view (which now also accepts an
optional --project for name-based lookup).
- Remove document create's duplicate local resolver in favor of the
shared one.
- Update --help to say "UUID, slug ID, or name" everywhere.

Closes #221

---------

Co-authored-by: Theo Gregory <theo@gregory.sh>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Schilling <code@schpet.com>
Co-authored-by: Bryan <bryandesigning@gmail.com>
Co-authored-by: Joseph <31323641+josephyooo@users.noreply.github.com>
Co-authored-by: Magnus Buvarp <magnus.buvarp@gmail.com>
Co-authored-by: Rengar Lee <Rengarlee@163.com>
Co-authored-by: Hyunsu Lim <hyunsu.lim01@gmail.com>
Co-authored-by: Luc Leray <luc.leray@gmail.com>
Co-authored-by: c-99-e <268417377+c-99-e@users.noreply.github.com>
Co-authored-by: Al Johri <al.johri@gmail.com>
Co-authored-by: Mihai Chiorean <mihai.v.chiorean@gmail.com>
Co-authored-by: Mihai Chiorean <mihai-chiorean@users.noreply.github.com>
Co-authored-by: Jeffrey Holm <jeff.holm@scale.com>
Co-authored-by: Paymahn Moghadasian <paymahn1@gmail.com>
Co-authored-by: Evan Jacobson <evanjacobson3@gmail.com>
Co-authored-by: schpetbot <bot@schpet.com>
Co-authored-by: Alex Broekhof <alex@quilt.com>
2026-07-11 14:58:31 -07:00
Peter Schilling 95097b8849 Follow-up to #170: cover labels on the with-comments query path
#170 adds `labels` to `issue view --json` by selecting them in both
GetIssueDetails and GetIssueDetailsWithComments. Its dedicated
labels-with-content test only exercises the --no-comments (GetIssueDetails)
path; the with-comments path was covered only with empty labels. Add a
--json (with comments) test carrying real labels so both fetchIssueDetailsRaw
code paths are verified to surface labels — the consistency both were meant to
guarantee.
2026-07-11 14:27:40 -07:00
Rengar Lee 96f0e04de2 feat: add labels to issue view --json output (#170)
Add `labels` field to the JSON output of `linear issue view --json`.

Each label includes `id`, `name`, and `color`.

## Example

```json
{
  "identifier": "P-51",
  "labels": [
    {
      "id": "031de1e8-...",
      "name": "Work",
      "color": "#4cb782"
    }
  ]
}

---------

Co-authored-by: Hyunsu Lim <hyunsu.lim01@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Luc Leray <luc.leray@gmail.com>
Co-authored-by: c-99-e <268417377+c-99-e@users.noreply.github.com>
Co-authored-by: Al Johri <al.johri@gmail.com>
Co-authored-by: Mihai Chiorean <mihai.v.chiorean@gmail.com>
Co-authored-by: Peter Schilling <code@schpet.com>
Co-authored-by: Mihai Chiorean <mihai-chiorean@users.noreply.github.com>
Co-authored-by: Jeffrey Holm <jeff.holm@scale.com>
Co-authored-by: Paymahn Moghadasian <paymahn1@gmail.com>
Co-authored-by: Evan Jacobson <evanjacobson3@gmail.com>
Co-authored-by: schpetbot <bot@schpet.com>
Co-authored-by: Alex Broekhof <alex@quilt.com>
Co-authored-by: Theo Gregory <theo@gregory.sh>
Co-authored-by: Bryan <bryandesigning@gmail.com>
Co-authored-by: Ryan Schumacher <j.r.schumacher@gmail.com>
Co-authored-by: Joseph <31323641+josephyooo@users.noreply.github.com>
Co-authored-by: Magnus Buvarp <magnus.buvarp@gmail.com>
2026-07-11 14:24:53 -07:00
Magnus Buvarp 9e0874847e Improve issue create project selection (#208)
## Summary

This improves `linear issue create` in three related areas:

- allow `--project` to keep issue creation interactive, the same way
`--parent` already does
- add optional project selection in interactive issue creation
- add configurable default self-assignment behavior for created issues

Before this change, `linear issue create --project "Dashboard"` skipped
interactive mode and failed with "Title is required when not using
interactive mode". With this PR, `--project` is treated as an
interactive-safe input, so users can preselect a project without also
needing `--title`.

Project prompting in interactive mode remains opt-in.
`issue_create_ask_project` defaults to `false`, so existing interactive
behavior is unchanged unless users explicitly enable the new prompt.

## Changes

- allow interactive issue creation when the only flags are `--project`,
`--parent`, or both
- add a team-scoped project picker during interactive issue creation
- gate the direct project prompt behind a new config option:
`issue_create_ask_project`
- keep `issue_create_ask_project = false` as the default, so project
selection stays out of the main interactive flow unless enabled
- when `issue_create_ask_project = false`, expose `Project` through the
existing "Add more fields" flow instead
- continue inheriting the parent issue's project when `--parent` is used
without an explicit `--project`
- allow explicit `--project` together with `--parent` and defer any
invalid combination checks to the Linear API
- add a new config option, `issue_create_assign_self`, with these modes:
  - `auto` (default): respect Linear's `autoAssignToSelf` setting
  - `always`: default-assign created issues to self
  - `never`: never default-assign created issues
- update docs for the new interactive behavior and config options

## Notes

- `issue_create_ask_project` defaults to `false`, so users who do not
set it will keep the previous interactive flow
- project selection only shows projects available to the selected team
- the interactive project picker is not shown when a parent issue is
present unless the project was explicitly provided
- explicit `--assignee`, the interactive assignee flow, and `--start`
still override the default assignment behavior

## Testing

- `deno task codegen`
- `deno task check`
- `deno lint`
- `deno fmt`
- `deno task generate-skill-docs`
- `deno test --allow-all --quiet test/config.test.ts`
- `deno test --allow-all --quiet
test/commands/issue/issue-create.test.ts`
2026-07-11 14:11:26 -07:00
Peter Schilling dec4ee5966 Follow-up to #235: exclude resolved inline comments from the update guard
#235 makes `document update` refuse a content replacement when the document has
active inline comments (whose anchors the replacement could orphan), with
--force to override, and exposes comments in `document view --json`. Two
corrections to the guard in document-update.ts:

- Exclude resolved/archived inline comments. The guard query only selected
  `quotedText`, so it blocked on ANY inline comment — including resolved
  (closed) threads whose anchor detaching loses no live context. It now also
  selects `resolvedAt`/`archivedAt` and ignores comments that are resolved or
  archived, so a closed thread no longer forces users to pass --force.
- Drop the hand-written `DocumentInlineComment*` interfaces in favor of the
  codegen-inferred type (`DocumentInlineCommentGuardQuery`), per the repo's
  no-`any`/typed-GraphQL convention. The annotation also breaks the circular
  result-type inference that the reused `after` cursor variable introduces
  (the reason the hand-written interface existed).

Adds a test that a resolved inline comment lets the update proceed without
--force.
2026-07-11 14:01:13 -07:00
Joseph 9756aafe19 Guard document updates when inline comments exist (#235)
## Summary

This makes Linear document comments visible to agents/automation and
prevents `linear document update` from silently orphaning inline comment
anchors.

- include paginated document comments in `linear document view <id>
--json`
- guard Markdown content updates when active inline document comments
are present
- allow top-level document comments to pass through, since they do not
carry losable inline anchors
- add `--force` to opt back into the existing replacement behavior when
the caller intentionally accepts the risk


## Background / related work

Closes #230, which reports that document comments are currently
unreachable from the CLI and omitted from `document view --json`.

Related:

- #219 added richer `document view` behavior for downloaded inline
images; this PR keeps normal/raw rendered views lean and only expands
`--json` with comment metadata.
- #121 added the raw `linear api` GraphQL escape hatch. That is useful
for inspection, but it does not make `documentUpdate(content)` safe for
inline document comments.

## API limitation

This is intentionally a safety guard plus read fix, not a
comment-preserving writer.

From the schema used by this CLI:

- `Document.comments(...)` exposes document comments and
`Comment.quotedText`, which is enough to detect inline comments.
- `DocumentUpdateInput` only accepts Markdown `content` for document
body writes; it does not accept `contentState`/YJS/ProseMirror data or
comment anchor metadata.
- `CommentCreateInput`/`CommentUpdateInput` expose `quotedText`, but
live testing showed raw GraphQL `commentUpdate(quotedText: ...)` returns
success without recreating an inline anchor. Writing
`<linear-comment>`-style tags through `documentUpdate(content)` stores
literal text, not an anchor.

So the CLI cannot preserve or restore inline anchors through the public
GraphQL write path it uses. The best safe behavior here is to make
comments visible and convert silent data loss into an explicit stop.
`--force` remains the old replacement behavior behind an intentional
flag.

## Behavior

`linear document view <id> --json` now returns a flattened, paginated
`comments.nodes` list with fields useful to agents:

- `id`, `body`, `quotedText`, `documentContentId`
- timestamps / archive / resolution metadata
- `url`, `user`, and `parent.id`
- final `pageInfo`

`linear document update <id> --content...` now scans active document
comments page-by-page. It blocks only when it finds a comment with
`quotedText`, i.e. an inline comment anchor that can be detached by
replacing Markdown content. Top-level document comments with
`quotedText: null` do not block.

## Testing

- `deno task validate`
- `deno test --allow-all --quiet`

Full suite result locally: `338 passed`, `0 failed`, `5 ignored`.
2026-07-11 13:58:36 -07:00
Peter Schilling 27e10af35f Follow-up to #228: harden --all pagination and fix TZ-flaky snapshots
#228 fixes milestone view silently capping its issue list, adding --all to
paginate. Two corrections:

1. Fail loudly on inconsistent --all pagination. The loop guarded on
   `while (pageInfo.hasNextPage && pageInfo.endCursor)` and `if (next == null)
   break`, so if Linear advertised another page but returned no cursor (or the
   milestone vanished mid-pagination), --all would stop early and return a
   *partial* list with no error — reintroducing the exact silent-truncation bug
   this command exists to fix. Now throws CliError / NotFoundError instead, with
   a regression test.

2. Fix timezone-flaky snapshots. The new "Truncated" and "--all paginates" tests
   used midnight-UTC timestamps (2020-01-01T00:00:00Z) whose snapshots were
   generated in a US timezone, so `formatRelativeTime`'s toLocaleDateString
   fallback rendered 12/31/2019 there but 1/1/2020 under CI's UTC — failing CI
   (fork CI never ran, so it slipped through). Moved the fixtures to noon UTC so
   they render the same date in every timezone; verified passing under TZ=UTC and
   TZ=America/Los_Angeles.
2026-07-11 13:46:11 -07:00
Ryan Schumacher 701a395bc6 fix(milestone): surface truncation in view, add --all flag (#222)
milestone view silently capped its issues list at 10 items. A user with
12 issues would see only 10 and could miss issues entirely when
iterating over the visible output in a script. The cap was also
undocumented in --help.

- Request first: 50 issues with pageInfo on the connection.
- Add --all to paginate through every page when needed.
- Show a prominent truncation footer pointing at --all and at
  `linear issue query --milestone X --json` for full programmatic
  access.
- Document the default cap in --help.

Closes #222
2026-07-11 13:43:25 -07:00
Peter Schilling 43a629029f Follow-up to #216: add error-path tests for project create fields
PR #216 adds --content/--content-file plus priority/label/member/icon/color
to `linear project create`, with happy-path snapshot tests and a
mutual-exclusion unit test. This adds the missing failure-path coverage that
both blind-planning passes called for:

- invalid --priority is rejected with a ValidationError before any network call
- an unknown --label surfaces a NotFoundError
- an unknown --member surfaces a NotFoundError

These use a stubbed Deno.exit and capture stderr (mirroring the validation
tests in issue-query.test.ts), and run against a mock Linear server so they
never touch the real API. No product code changes — the contributor's feature
is correct as written; this only locks in the error behavior.
2026-07-11 13:16:10 -07:00
Bryan ad630c5d69 feat(project): support overview content on create (#216)
## Why

Linear projects have two text fields: a short description and a longer
project overview. The CLI only supported the short description, so
projects created from the terminal still needed a manual edit in Linear
to add goals, plans, specs, or launch notes.

This PR lets users create a project with its overview already filled in,
either from inline markdown or a markdown file. That makes `linear
project create` more useful for scripted project setup, templates, and
project specs kept in a repo.

It also exposes a few existing Linear create fields so users can set
priority, labels, members, icon, and color when creating the project
instead of doing a follow-up edit.

## What changed

- `linear project create` now accepts `--content` for inline project
overview markdown.
- `--content-file` reads the project overview from a markdown file.
- `--content` and `--content-file` are mutually exclusive.
- Project create can now set priority, labels, members, icon, and color.
- The command passes these values through `ProjectCreateInput` using
Linear field names.

## Checks

- `deno task codegen`
- `deno task check`
- `deno lint`
- `deno task test`
2026-07-11 13:13:23 -07:00
Peter Schilling f5d87b8884 Follow-up to #234: validate --public per-batch before any upload
PR #234 makes attachment uploads private by default and adds a --public
opt-in that errors (rather than silently downgrades) for non-image types.
Two gaps remained in `issue comment add`, both flagged independently while
blind-planning the fix:

- The public/type check only ran inside uploadFile, per file, at upload
  time. With `--public` and multiple attachments, an earlier valid image
  was uploaded *publicly* before a later non-image threw — a partial, and
  unwanted-public, upload. Pre-flight resolveMakePublic() for every
  attachment in the existing existence-validation loop so a mixed batch
  fails before anything is uploaded.
- `--public` with no `--attach` was a silent no-op. Per the repo's
  "explicit invalid input should error" convention, reject it.

Adds command-level tests for both validation paths (they short-circuit
before any network call, so no upload mock is needed).
2026-07-11 12:47:45 -07:00
Theo Gregory dc2dca42f1 fix(upload): default attachments to private, add --public opt-in
Image attachments uploaded via `issue attach` and `issue comment add
--attach` were sent with makePublic auto-detected to true for raster
images, producing a public.linear.app URL readable by anyone,
unauthenticated, with no way to opt out. This silently published
screenshots of internal data from private workspaces.

Default all uploads to private (uploads.linear.app), matching the Linear
web app. Add a --public flag to both commands to opt into a public URL,
which is only valid for raster images; requesting it for other types is
now an error rather than a silent downgrade. Print a warning whenever an
upload lands on a public URL.

Also document the attachment commands and their privacy behaviour in the
README (previously undocumented) and regenerate the skill reference.

Fixes #233

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 12:45:00 -07:00
Peter Schilling fc85b919cd feat(issue): show blocked indicator in mine and query output
Add a 'B' column (with ⊘) to 'linear issue mine' and 'linear issue query'
that flags issues with at least one active blocker — a blocked-by relation
whose blocker is not in a completed or canceled workflow state. The
inverseRelations connection is also surfaced in --json output.
2026-05-19 15:55:54 -07:00
Peter Schilling eb8a659f04 feat(document): download attached images in document view
Document view's rendered/raw output now downloads inline images and
Linear-upload links to the same /tmp cache used by issue view, so terminal
renderers and downstream tools see local file paths instead of remote URLs
that require auth.

Image helpers (extractImageInfo, extractLinearLinkInfo, replaceImageUrls,
getUrlHash, getLinearUploadHost, downloadMarkdownImages) move from
src/commands/issue/issue-view.ts to src/utils/markdown-images.ts so both
commands share one implementation. downloadIssueImages becomes
downloadMarkdownImages, which takes an array of markdown sources rather
than an issue-specific (description, comments) tuple.

Adds --no-download to document view (mirroring issue view) and reorders
the command so the download step runs after the --json early return but
before --raw, matching issue view's behavior where piped output also gets
local paths.

Also adds remark-gfm to the parse/stringify pipeline used by
replaceImageUrls so GFM constructs (task lists, tables, strikethrough)
survive the URL rewrite. Without it, remark-stringify would re-escape
`- [ ] todo` as `* \[ ] todo` whenever an image is rewritten, mangling
documents and issue descriptions that lean on GFM syntax.
2026-05-19 12:54:29 -07:00
schpetbot f7922d4121 ci: drop skill docs staleness check, keep generation smoke test (#218)
The skill docs check was failing because `Deno.consoleSize()` returns
different values depending on the runner's pseudo-terminal dimensions
(CI is wide, local terminals vary). Since the docs are for LLM
consumption and not correctness, dropping the `git diff` check and just
verifying generation doesn't crash.

Co-authored-by: Alex Broekhof <alex@quilt.com>
Co-authored-by: Peter Schilling <code@schpet.com>
2026-05-15 03:54:40 +00:00
Peter Schilling 027f25e30c chore: pin deno to 2.7.9 via mise.toml and ci workflow 2026-05-13 20:51:13 -07:00
Peter Schilling b3b9960436 fix: order issue view comments chronologically
Linear's comments connection returns newest-first and the GraphQL schema
doesn't expose a direction argument, so sort root comments ascending
client-side to match the order shown in Linear's UI.
2026-04-21 15:53:19 -07:00
Peter Schilling 90767cbd07 chore: add plugin version bump step to release skill 2026-04-03 13:03:47 -07:00
Peter Schilling e5fa8d28da chore: bump plugin version to 2.0.0 2026-04-03 12:56:16 -07:00