linear-sdk-cli 0.1.3 → 0.2.1

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 (79) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +84 -58
  4. package/COVERAGE.md +5 -5
  5. package/MIGRATING.md +71 -69
  6. package/README.md +115 -90
  7. package/package.json +6 -7
  8. package/skills/README.md +15 -0
  9. package/skills/linear-sdk-cli/SKILL.md +27 -3
  10. package/skills/linear-sdk-cli/references/api.md +9 -9
  11. package/skills/linear-sdk-cli/references/auth.md +18 -0
  12. package/skills/linear-sdk-cli/references/commands.md +2 -2
  13. package/skills/linear-sdk-cli/references/comment.md +9 -6
  14. package/skills/linear-sdk-cli/references/config.md +9 -11
  15. package/skills/linear-sdk-cli/references/issue.md +72 -18
  16. package/skills/linear-sdk-cli/references/open.md +29 -0
  17. package/skills/linear-sdk-cli/references/team.md +16 -0
  18. package/skills/linear-sdk-cli/scripts/generate-docs.ts +6 -2
  19. package/skills/linear-sdk-cli-maintenance/SKILL.md +93 -0
  20. package/src/bin/linear.ts +16 -6
  21. package/src/cli.ts +3 -1
  22. package/src/commands/agent-session.ts +13 -5
  23. package/src/commands/api.ts +15 -9
  24. package/src/commands/attach.ts +15 -4
  25. package/src/commands/comment.ts +50 -11
  26. package/src/commands/completion.ts +2 -6
  27. package/src/commands/cycle.ts +6 -1
  28. package/src/commands/discover.ts +6 -2
  29. package/src/commands/document.ts +4 -1
  30. package/src/commands/initiative.ts +5 -1
  31. package/src/commands/issue.ts +392 -91
  32. package/src/commands/meta.ts +139 -31
  33. package/src/commands/milestone.ts +4 -1
  34. package/src/commands/notification.ts +15 -6
  35. package/src/commands/open.ts +84 -0
  36. package/src/commands/project.ts +17 -4
  37. package/src/commands/team.ts +13 -0
  38. package/src/commands/user.ts +2 -2
  39. package/src/commands/webhook.ts +12 -2
  40. package/src/config.ts +501 -184
  41. package/src/context.ts +13 -4
  42. package/src/git.ts +28 -3
  43. package/src/lib/body.ts +26 -2
  44. package/src/lib/errors.ts +33 -8
  45. package/src/lib/introspect.ts +76 -7
  46. package/src/lib/keyring.ts +51 -9
  47. package/src/lib/mentions.ts +49 -0
  48. package/src/lib/open.ts +32 -0
  49. package/src/lib/options.ts +108 -12
  50. package/src/lib/output-shapes.ts +81 -9
  51. package/src/lib/pagination.ts +50 -17
  52. package/src/lib/resolve.ts +40 -7
  53. package/src/lib/shape.ts +16 -12
  54. package/src/lib/upload.ts +3 -1
  55. package/src/output/color.ts +52 -0
  56. package/src/output/format.ts +96 -18
  57. package/src/output/markdown.ts +133 -0
  58. package/src/output/pager.ts +131 -0
  59. package/src/output/sanitize.ts +1 -1
  60. package/src/output/table.ts +7 -7
  61. package/src/services/agent-session.ts +11 -3
  62. package/src/services/attachment.ts +14 -5
  63. package/src/services/comment.ts +37 -9
  64. package/src/services/cycle.ts +11 -4
  65. package/src/services/document.ts +29 -10
  66. package/src/services/favorite.ts +15 -9
  67. package/src/services/initiative-update.ts +16 -15
  68. package/src/services/initiative.ts +15 -10
  69. package/src/services/issue.ts +263 -19
  70. package/src/services/label.ts +12 -2
  71. package/src/services/milestone.ts +8 -5
  72. package/src/services/notification.ts +16 -2
  73. package/src/services/organization.ts +23 -18
  74. package/src/services/project.ts +55 -34
  75. package/src/services/roadmap.ts +7 -6
  76. package/src/services/state.ts +9 -3
  77. package/src/services/team.ts +67 -24
  78. package/src/services/user.ts +2 -2
  79. package/src/services/webhook.ts +28 -17
@@ -14,7 +14,7 @@
14
14
  "name": "Eugene Beloded"
15
15
  },
16
16
  "source": "./",
17
- "skills": ["./skills/linear-sdk-cli"]
17
+ "skills": ["./skills/linear-sdk-cli", "./skills/linear-sdk-cli-maintenance"]
18
18
  }
19
19
  ]
20
20
  }
@@ -5,5 +5,5 @@
5
5
  "author": {
6
6
  "name": "Eugene Beloded"
7
7
  },
8
- "skills": ["./skills/linear-sdk-cli"]
8
+ "skills": ["./skills/linear-sdk-cli", "./skills/linear-sdk-cli-maintenance"]
9
9
  }
package/CHANGELOG.md CHANGED
@@ -4,17 +4,38 @@ All notable changes to this project are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [0.1.3](https://github.com/zhendalf/linear-sdk-cli/compare/v0.1.2...v0.1.3) (2026-08-24)
7
+ ## [0.2.1](https://github.com/zhendalf/linear-sdk-cli/compare/v0.2.0...v0.2.1) (2026-08-24)
8
+
9
+ ### Bug Fixes
10
+
11
+ - format generated release notes ([3bab1dd](https://github.com/zhendalf/linear-sdk-cli/commit/3bab1dda627cd83d1feb8a2e1c0a7da37bee10db))
12
+
13
+ ## [0.2.0](https://github.com/zhendalf/linear-sdk-cli/compare/v0.1.3...v0.2.0) (2026-08-24)
14
+
15
+ ### Features
8
16
 
17
+ - expand CLI workflows and terminal UX ([0fe31cc](https://github.com/zhendalf/linear-sdk-cli/commit/0fe31cca7e71d0fa6eee2b7c1461ecaf0a01103f))
18
+
19
+ ### Bug Fixes
20
+
21
+ - **ci:** give release dispatch repository context ([#15](https://github.com/zhendalf/linear-sdk-cli/issues/15)) ([e3f51e6](https://github.com/zhendalf/linear-sdk-cli/commit/e3f51e600102416f3bd3212578b364a13a452e49))
22
+
23
+ ## [0.1.3](https://github.com/zhendalf/linear-sdk-cli/compare/v0.1.2...v0.1.3) (2026-08-24)
9
24
 
10
25
  ### Bug Fixes
11
26
 
12
- * **ci:** grant OIDC to automated publisher ([#7](https://github.com/zhendalf/linear-sdk-cli/issues/7)) ([acc6e4b](https://github.com/zhendalf/linear-sdk-cli/commit/acc6e4b9c4ab485ce39eeec7eb2c835892016e59))
27
+ - **ci:** grant OIDC to automated publisher ([#7](https://github.com/zhendalf/linear-sdk-cli/issues/7)) ([acc6e4b](https://github.com/zhendalf/linear-sdk-cli/commit/acc6e4b9c4ab485ce39eeec7eb2c835892016e59))
13
28
 
14
29
  ## [Unreleased]
15
30
 
16
31
  ### Added
17
32
 
33
+ - **Explicit, notification-capable comment mentions (`--mention`, TES-738).** Ordinary `@name`
34
+ text remains literal Markdown. Comment-writing commands now accept repeatable
35
+ `--mention <name|email|me|id>`, resolve each user exactly, deduplicate repeats, and prepend real
36
+ Linear mention tokens as their own paragraph. The option is available on `issue comment`,
37
+ `comment add`, `comment reply`, and `comment update`, including the shared
38
+ `issue comment add/update` mounts.
18
39
  - **Every command declares its `--json` output, and `linear commands <path>` prints it (TES-610).**
19
40
  An agent driving the CLI cold guessed key names and filed false bugs (`.user` read as `.author`,
20
41
  `.parent` as `.parentId`); the field names were knowable only from the source. Each node of
@@ -23,7 +44,7 @@ All notable changes to this project are documented here. The format is based on
23
44
  `completion`), `fields` (key → `"string"`, `"number|null"`, `["string"]`, nested `{…}`,
24
45
  `{nullable: {…}}` for a relation that may be null, `"comments?"` for a key present only
25
46
  sometimes) and `variants` (the whole shape under `--web`, `--start`, `op=list`, …). New `linear
26
- commands <path...>` describes one command — a bare object under `--json` (`.output.fields`),
47
+ commands <path...>` describes one command — a bare object under `--json` (`.output.fields`),
27
48
  or usage, options and one `key: type` line per field for a human, plus its subcommands for a
28
49
  group; an unknown path is `not_found` (exit 3) with the nearest paths. The shapes are declared
29
50
  beside the interfaces they describe (`shape<IssueRow>({…})`, `src/lib/shape.ts`), where a field
@@ -68,9 +89,9 @@ All notable changes to this project are documented here. The format is based on
68
89
  TES-642).** `initiative add-project <initiative> <project>` (`--sort-order`) and
69
90
  `remove-project` (confirmed; finds the link on the project's side, so no workspace-wide paging)
70
91
  and `initiative unarchive <id>` (resolves among archived initiatives, refuses a live one). `initiative
71
- list` gains `--status`, `--owner`, `--archived` — the list stays every-status by default, and
92
+ list` gains `--status`, `--owner`, `--archived` — the list stays every-status by default, and
72
93
  the reference CLI's `--all-statuses` is accepted as the no-op it is here. `initiative
73
- create/update` take `--icon`/`--color`, and `initiative view` shows `icon`, `archivedAt` and
94
+ create/update` take `--icon`/`--color`, and `initiative view` shows `icon`, `archivedAt` and
74
95
  the linked `projects` (`{id, name, status}`).
75
96
  - **`project list --all-teams`, `team create --private`, health-only status updates (TES-642).**
76
97
  `project list` is scoped to the default team and the only way out was the accidental
@@ -111,7 +132,7 @@ All notable changes to this project are documented here. The format is based on
111
132
  a repository; `--path` to choose), taking `--team` or offering the workspace's teams in a prompt;
112
133
  it refuses to replace an existing file without `--force`. `config set <key> <value>` changes one
113
134
  of `team`, `workspace`, `sort`, `vcs` in the project config discovery would actually read — a
114
- schpet-written `.config/linear.toml` included, in *its* spelling (`team_id`, `issue_sort`), so no
135
+ schpet-written `.config/linear.toml` included, in _its_ spelling (`team_id`, `issue_sort`), so no
115
136
  second competing key appears — or, with `--user`, in `~/.config/linear/config.toml`. The edit is
116
137
  textual, one line replaced or appended before the first table, so comments and layout survive;
117
138
  the result is parsed back to prove it, falling back to a full re-serialize only if a layout we
@@ -132,7 +153,7 @@ All notable changes to this project are documented here. The format is based on
132
153
  in exactly one state, so intersecting them would build a filter that can never match. A single
133
154
  `--team`/`--state` sends the exact filter it always did (`key: { eq }` / `type: { eq }`);
134
155
  several send `key: { in: [...] }` and an `or` of state clauses. `--state` keeps taking a state
135
- *name* or a state *type*, mixed freely: names stay individual `eqIgnoreCase` clauses because
156
+ _name_ or a state _type_, mixed freely: names stay individual `eqIgnoreCase` clauses because
136
157
  `in` is exact-case, so `--state 'in progress'` still matches "In Progress".
137
158
  `--team` is repeatable **only on the three issue queries**. It is a global option that ~135
138
159
  commands use for something other than filtering — the team an issue is created in, the team a
@@ -146,7 +167,7 @@ All notable changes to this project are documented here. The format is based on
146
167
  ISO 8601. The date is validated locally first: `new Date()` accepts "1", "March 2024" and
147
168
  "yesterday", and a garbage bound is not an API error but an empty result set — a filter that
148
169
  silently matches nothing looks exactly like a query with no matches.
149
- - **`--project-label`** — every issue whose *project* carries a label, matched case-insensitively
170
+ - **`--project-label`** — every issue whose _project_ carries a label, matched case-insensitively
150
171
  (`--project-label backend` finds the "Backend" label). Mutually exclusive with `--project`:
151
172
  one names a single project, the other a set of them.
152
173
  - **`--search-comments`** (`issue search` only) — match comment bodies as well as titles and
@@ -159,7 +180,7 @@ All notable changes to this project are documented here. The format is based on
159
180
  only changes precision: with `--project` the name is resolved to a milestone id inside that
160
181
  project, without it the milestone is matched by name across projects. A transplanted command
161
182
  always passes `--project` and behaves identically either way.
162
- - **`issue update --team <key>` now actually moves the issue between teams** (AUDIT.md #8: the flag
183
+ - **`issue update --team <key>` now actually moves the issue between teams** (the flag
163
184
  was accepted and silently dropped — alone it produced a misleading "Nothing to update", and
164
185
  alongside another flag it moved nothing at all). Everything team-scoped in the same command is
165
186
  resolved against the **destination** team, so `issue update TES-42 --team ENG --state 'In Review'`
@@ -179,12 +200,12 @@ All notable changes to this project are documented here. The format is based on
179
200
  - **Short flags.** `-j` for the global `--json` (the reference's most-used flag, on 17 of its
180
201
  commands) and `-w` for `--web` on `issue view` / `issue pull-request`. Both letters were
181
202
  free in our tree, so neither costs an existing meaning — we still hold one meaning per
182
- letter everywhere, which is exactly why we are *not* adopting their `-t`/`-p`/`-a`/`-l`/`-s`
183
- (see `ALIGNMENT.md`: their own tree spells `-t` as both `--title` and `--team`).
203
+ letter everywhere, which is exactly why we are _not_ adopting their `-t`/`-p`/`-a`/`-l`/`-s`
204
+ (their own tree spells `-t` as both `--title` and `--team`).
184
205
  - **Long-flag spellings.** `--due-date` (`issue create`/`update`), `--target-date` (`project`,
185
206
  `milestone`, `initiative` create/update), `--start-date` (`project create`/`update`),
186
207
  `--search` (`issue list`/`mine`), and `--all-states` on `issue list`, where it is the
187
- no-op it describes — `list` already spans every state. Aliases are registered *hidden*, so
208
+ no-op it describes — `list` already spans every state. Aliases are registered _hidden_, so
188
209
  `--help` and `linear commands --json` keep advertising exactly one spelling per flag; the
189
210
  full mapping lives in README's "Coming from linear-cli". Passing both spellings at once is a
190
211
  usage error rather than a silent pick: `--due 2026-01-01 --due-date 2026-02-01` is not a typo
@@ -203,12 +224,12 @@ All notable changes to this project are documented here. The format is based on
203
224
  `issue comment` gained `add`/`list`/`update`/`delete` subcommands mounted on the same
204
225
  handlers as the top-level `comment` group, so there is one implementation, not two.
205
226
  `issue comment <id> <body>` is unchanged — commander only dispatches to a subcommand when
206
- the *first* operand is one of those four names, which is the one behavior this touches: a
227
+ the _first_ operand is one of those four names, which is the one behavior this touches: a
207
228
  lone `linear issue comment add` used to mean "comment the word 'add' on the branch's issue"
208
229
  and now reaches the subcommand (and says so loudly). The short aliases `ls`/`edit`/`rm` are
209
230
  deliberately not registered under `issue comment`, to keep that collision at four words.
210
231
  - **`issue mine` — your unstarted work.** The reference `linear-cli` makes this its
211
- *default* listing (`issue list` there is an alias of `mine`), so a script or a habit carried
232
+ _default_ listing (`issue list` there is an alias of `mine`), so a script or a habit carried
212
233
  over from it silently saw only your own unstarted issues. We keep `list` general — a command
213
234
  named "list" should list — and add `mine` next to it with the reference's defaults: the
214
235
  viewer's issues, restricted to `unstarted` states, widened by `--all-states`. It takes the same
@@ -273,9 +294,9 @@ All notable changes to this project are documented here. The format is based on
273
294
  resolved by name (a team template outranks a shared one of the same name; a miss lists what
274
295
  there is), sent as `templateId`; every value you pass alongside overrides what the template
275
296
  fills. **The team's default template is now applied**, as Linear's own new-issue form and the
276
- reference CLI do: the API applies it *only when asked* (verified live — a plain `issueCreate` on
297
+ reference CLI do: the API applies it _only when asked_ (verified live — a plain `issueCreate` on
277
298
  a team with a default template returns an issue with no description), so `useDefaultTemplate:
278
- true` is sent unless `--no-default-template` (also accepted under the reference's spelling,
299
+ true` is sent unless `--no-default-template` (also accepted under the reference's spelling,
279
300
  `--no-use-default-template`); an explicit `--template` replaces it rather than stacking. A
280
301
  `--parent` child now **joins its parent's project** unless `--project` says otherwise — the
281
302
  sub-issues we created landed outside the project their parent was in — and `--milestone` can
@@ -287,13 +308,18 @@ All notable changes to this project are documented here. The format is based on
287
308
 
288
309
  ### Changed
289
310
 
311
+ - **Development linting now uses Oxlint instead of ESLint.** The migrated configuration preserves
312
+ the existing recommended JavaScript and TypeScript checks and underscore-prefixed unused-variable
313
+ exceptions while reducing the lint dependency tree and execution time.
314
+ - **TypeScript 7 now powers type-checking.** The native compiler replaces TypeScript 6 without
315
+ source or configuration compatibility changes in this project.
290
316
  - **`--fields`, `--limit` and `--all` are refused on commands that never read them** (TES-637 (2),
291
317
  TES-596). All twelve globals are registered on every command so they can sit anywhere on the
292
- line, but `--fields` projects a *rendered* result (a table or a detail block) and `--limit`/`--all`
293
- cap or exhaust a *paged* query; on the ~90 commands that print a receipt (every mutation, `issue
294
- id`, `commands`, …) all three used to vanish without a word. That silence cost data: schpet's `-f`
318
+ line, but `--fields` projects a _rendered_ result (a table or a detail block) and `--limit`/`--all`
319
+ cap or exhaust a _paged_ query; on the ~90 commands that print a receipt (every mutation, `issue
320
+ id`, `commands`, …) all three used to vanish without a word. That silence cost data: schpet's `-f`
295
321
  is `--description-file` on `project create`, so `linear project create --name X -f desc.md`
296
- parsed `-f` as *our* `--fields`, created the project with **no description**, and exited 0. Now a
322
+ parsed `-f` as _our_ `--fields`, created the project with **no description**, and exited 0. Now a
297
323
  root `preAction` hook (`assertGlobalsApply` in `lib/options.ts`, driven by two applicability
298
324
  tables keyed by command path — `FIELDS_COMMANDS`, `LIMIT_COMMANDS`) rejects them before the
299
325
  action runs, with a usage error that names the command and, where the command has one, the flag
@@ -302,7 +328,7 @@ All notable changes to this project are documented here. The format is based on
302
328
  `project create -n 5` gets "-n is --limit here; the name is --name". Every renderer and paged
303
329
  query is unchanged; the tables are pinned against the real program tree so a rename shows up
304
330
  in the tests, and a renderer missing from the table fails loudly (its `--fields` is refused),
305
- never quietly. `--team` is deliberately *not* guarded this way: `alias lin='linear -t TES'` is a
331
+ never quietly. `--team` is deliberately _not_ guarded this way: `alias lin='linear -t TES'` is a
306
332
  real usage pattern and must keep working on workspace-scoped commands. Not adopted: a local
307
333
  `-f = --description-file` on the four commands — one letter, two meanings (see MIGRATING.md §5).
308
334
  - **`project create -t/--team` collects, as `--teams` does** (TES-637 (3)). The global `--team` is
@@ -310,7 +336,7 @@ All notable changes to this project are documented here. The format is based on
310
336
  `--team A --team B` created the project in **B alone**. `--team` is now declared locally there,
311
337
  repeatable and comma-separated, one list with `--teams`; both flags at once is a usage error
312
338
  ("Pass either --teams or --team, not both"). `project update --team` stays refused (that list
313
- *replaces* the project's teams; the message points at `--teams`).
339
+ _replaces_ the project's teams; the message points at `--teams`).
314
340
  - **`user list --all` / `team members --all` say what `--all` did not do** (TES-637 (1)). schpet's
315
341
  `--all` there means "include inactive members"; ours is the global "exhaust pagination", and
316
342
  deactivated users are `--include-disabled`. Same line, both exit 0, different rows. `--all` keeps
@@ -328,17 +354,17 @@ All notable changes to this project are documented here. The format is based on
328
354
  the PR `ID Title` and stops copying the issue description into GitHub** (TES-637 (5)).
329
355
  `describe` used to print `Title`, a blank line, `Fixes ID`; it now prints `ID Title`, a blank
330
356
  line, `Linear-issue: Fixes ID`, `Linear-issue-url: <url>` — git trailers that `git
331
- interpret-trailers` / `git log --format=%(trailers:key=Linear-issue)` and jj's `trailers`
357
+ interpret-trailers` / `git log --format=%(trailers:key=Linear-issue)` and jj's `trailers`
332
358
  template read back, while Linear still sees the magic word directly before the id
333
359
  (linear.app/docs/github lists `fixes` and `references` among the recognized words) and links
334
360
  and closes the issue. `-r/--references` swaps the word. `--json` gains `url` and `message` (the
335
361
  full text as printed). `pull-request`: title `<ID> <title>` (a custom `--title` is prefixed the
336
362
  same way, as there); body the same two trailers — schpet sends the bare URL and relies on the
337
363
  branch name for the link, the `Fixes` line keeps the link (and auto-close) when the branch was
338
- renamed. The issue *description* is no longer pasted into the PR body: a GitHub PR is a wider
364
+ renamed. The issue _description_ is no longer pasted into the PR body: a GitHub PR is a wider
339
365
  audience than a Linear issue, and Linear links from the trailer, not the prose.
340
366
  - **A team-scoped listing with no team says it is listing every team's** (TES-637 (8)). `issue
341
- list`/`mine`/`search` and `project list` with no `--team`, no configured team and no
367
+ list`/`mine`/`search` and `project list` with no `--team`, no configured team and no
342
368
  `--all-teams` list the whole workspace — by design; schpet errors there ("No default team…"), so
343
369
  someone arriving from it could read the workspace as the team. One `info` line on stderr ("No
344
370
  default team configured; listing every team's. Pass --team <KEY> …"), silenced by `--quiet`,
@@ -385,7 +411,7 @@ All notable changes to this project are documented here. The format is based on
385
411
  model has no `team`/`cycle` getter (both `[Internal]` in the schema, though they work with a
386
412
  plain API key), and the previous view spent three lazy fetches on what one query selects.
387
413
  - **Repeating `--label` now narrows instead of broadening.** `--label bug --label regression`
388
- used to return issues carrying *either* label; it now returns only those carrying *both*.
414
+ used to return issues carrying _either_ label; it now returns only those carrying _both_.
389
415
  Every other repeatable filter in this CLI narrows, the reference CLI narrows, and the broadening
390
416
  reading was the surprising one — a ported script got a superset of what it asked for and no
391
417
  error to notice. A single `--label` is unchanged, and both forms stay case-insensitive.
@@ -396,7 +422,7 @@ All notable changes to this project are documented here. The format is based on
396
422
  workflow state **ascending** (active work above the backlog), then priority descending
397
423
  (no-priority last), then manual ascending. Membership is unchanged; only the ordering moved.
398
424
  `--sort updated` and `--sort created` (which the reference lacks) are untouched.
399
- The reference CLI hardcodes state *descending* for this flag, which the API answers with Backlog
425
+ The reference CLI hardcodes state _descending_ for this flag, which the API answers with Backlog
400
426
  **before** In Progress — a Low-priority backlog item outranks an Urgent in-progress one there.
401
427
  We match the intent (state-grouped, UI-like) rather than that payload; verified against the API.
402
428
  - **`issue search` takes filters, and is now team-scoped by default.** Linear's `searchIssues`
@@ -441,7 +467,7 @@ All notable changes to this project are documented here. The format is based on
441
467
 
442
468
  - **`issue list`/`mine`/`search` rows omitted `milestone` and `cycle`** (TES-652). The list
443
469
  query selected project and labels but neither relation, so `issue list --project X --json | jq
444
- 'group_by(.milestone.name)'` put every issue in the null bucket while `issue view` showed the
470
+ 'group_by(.milestone.name)'` put every issue in the null bucket while `issue view` showed the
445
471
  milestone. Rows now carry `milestone: {id, name} | null` and `cycle: {id, number, name} | null`
446
472
  — the detail's exact object shapes, from the one existing query (two selections, no extra
447
473
  request; `LIST_QUERY` and `SEARCH_QUERY` share the relation block so they cannot drift again).
@@ -462,7 +488,7 @@ All notable changes to this project are documented here. The format is based on
462
488
  Non-interactively, `comment update <id>` with no body is a usage error, not an editor.
463
489
  - **`issue comment "<body>"` on a matching branch failed** (TES-619) — the README's headline
464
490
  example. `issue comment [id] [body]` read a lone operand as the id, so `linear issue comment
465
- 'shipped'` on `tes-615-…` produced "No comment body provided" (and, in a terminal, opened
491
+ 'shipped'` on `tes-615-…` produced "No comment body provided" (and, in a terminal, opened
466
492
  `$EDITOR` for the body first and only then rejected `'shipped'` as an id — work lost).
467
493
  `assign` and `state` already disambiguated a lone operand; `comment` now does the same: one
468
494
  operand that looks like an issue id is the id (body from `--body-file` or the editor), anything
@@ -492,7 +518,7 @@ All notable changes to this project are documented here. The format is based on
492
518
  `--help`; a bare group shows its help** (TES-633). The root has an action (bare `linear` shows the
493
519
  branch's issue) and `issue` has a default subcommand (`view`), so commander's own "unknown
494
520
  command" never fired for either: `linear issues list` was `too many arguments. Expected 0
495
- arguments but got 2: issues, list.` and `linear issue lst` was `'lst' is not a valid issue id`.
521
+ arguments but got 2: issues, list.` and `linear issue lst` was `'lst' is not a valid issue id`.
496
522
  Now: `Unknown command 'issues'. Did you mean 'issue'? Run 'linear --help' to see the commands.`
497
523
  and `'lst' is not a valid issue id (…). Did you mean 'linear issue list'?` — a prefix (`proj`),
498
524
  a small typo (`lable`, `isue`) or an alias (`docs`, `notif`) is close enough; two-letter aliases
@@ -510,7 +536,7 @@ All notable changes to this project are documented here. The format is based on
510
536
  window-title rewrite into a title, and every teammate's `linear issue list` ran it — verified live
511
537
  on a `clitest-esc` issue whose title carried `\e[31m` and an OSC-8 link. Every string that
512
538
  reaches a person now passes through one function (`sanitizeForTerminal`, `src/output/sanitize.ts`)
513
- that strips *whole* sequences — CSI, OSC (BEL- or ST-terminated), DCS/SOS/PM/APC, other `ESC`
539
+ that strips _whole_ sequences — CSI, OSC (BEL- or ST-terminated), DCS/SOS/PM/APC, other `ESC`
514
540
  sequences, and their 8-bit C1 spellings — so `\e[31m` vanishes rather than leaving `[31m`
515
541
  behind, then whatever C0/C1/DEL and bidi-override characters remain (`\n` and `\t` stay: a
516
542
  description is multi-line). It is applied in `cell()` (every table cell and detail value) and in
@@ -527,7 +553,7 @@ All notable changes to this project are documented here. The format is based on
527
553
  resolves the name inside that project (names are unique per project only, the same rule
528
554
  `issue update --milestone` applies); a name without `--project` is a usage error that says so:
529
555
  `'…' is not a milestone id; pass --project <name|id> to look a milestone up by name.` `state view
530
- <name-or-type>` resolves against `--team` or the configured default team, exactly as
556
+ <name-or-type>` resolves against `--team` or the configured default team, exactly as
531
557
  `issue create --state` does, and says to pass `--team` when there is neither. UUIDs still go
532
558
  straight through.
533
559
  - **`milestone list`/`view` printed progress ×100 — `3846%`** (TES-648, found while fixing the
@@ -535,7 +561,7 @@ All notable changes to this project are documented here. The format is based on
535
561
  `Project.progress`, which is a fraction; the milestone renderers multiplied it by 100 anyway. The
536
562
  human output now reads `38%`; the JSON value stays as the API sends it.
537
563
  - **Detail views are one request each, not six to sixteen** (TES-622). Lists have always used a
538
- tailored GraphQL query, but every *detail* path awaited the SDK model's lazy relation getters,
564
+ tailored GraphQL query, but every _detail_ path awaited the SDK model's lazy relation getters,
539
565
  each its own HTTP round-trip. Measured live with a fetch counter before/after:
540
566
  `getIssueDetail(TES-601)` **8 → 1**; `getProjectDetail(linear-sdk-cli)` **7 → 1**;
541
567
  `getMilestoneDetail` with 13 issues **16 → 1** (it awaited `issue.state` per issue, so `-n 50`
@@ -561,14 +587,14 @@ All notable changes to this project are documented here. The format is based on
561
587
  - **Rate-limit waits are announced through the Context's `Output`** rather than a bare stderr
562
588
  writer, so the "rate limited; retrying in Ns" line honours `--quiet` like every other status line
563
589
  and can never land on the JSON stdout a script is parsing.
564
- - **Mutations could report a success the API never gave** (AUDIT.md #6). Every Linear mutation
590
+ - **Mutations could report a success the API never gave.** Every Linear mutation
565
591
  answers with a payload carrying `success: Boolean!`, and almost nothing here read it. The
566
- create/update paths did have a guard, but it tested whether the *entity* came back, not whether
592
+ create/update paths did have a guard, but it tested whether the _entity_ came back, not whether
567
593
  the write happened — so a `{success: false}` that still carried an entity walked through it — and
568
594
  the deletes, archives, subscribes and notification writes discarded the payload unread. Driven
569
595
  against a client whose every write is refused, **50 of the 51 mutating service entry points
570
596
  resolved happily**; `comment delete` was the single one that checked. The sharpest case was
571
- `issue update`, which fell back to the issue it had resolved *before* the mutation, so a
597
+ `issue update`, which fell back to the issue it had resolved _before_ the mutation, so a
572
598
  `{success: false, issue: null}` printed `Updated TES-1` and exited **0** — the shell would run the
573
599
  `&&` side of a command that changed nothing. All of it now goes through one helper
574
600
  (`unwrapMutation` / `assertMutation`, `src/lib/mutation.ts`) that requires `success === true` and,
@@ -583,27 +609,27 @@ All notable changes to this project are documented here. The format is based on
583
609
  to print `✓ Marked … read` and `{"read": true}` whatever the payload said.
584
610
 
585
611
  - **`notification read-all` claimed every notification was marked read.** It hardcoded
586
- `success: true` and reported the number of *unread* notifications it found, not the number it
612
+ `success: true` and reported the number of _unread_ notifications it found, not the number it
587
613
  actually marked, so a batch where some writes were refused looked identical to one where none
588
614
  were. It now derives the aggregate: `count` is how many really went through, `attempted` how many
589
615
  it tried, and `failed` lists the ones that did not with the API's reason. A single refusal no
590
616
  longer aborts the rest — it is reported instead of hidden, and the human output names each one.
591
617
 
592
- - **`milestone view` reported `issuesTruncated: false` while hiding issues** (AUDIT.md #5). The
593
- truncation check read `pageInfo.hasNextPage` off the connection *after* collecting from it, and
618
+ - **`milestone view` reported `issuesTruncated: false` while hiding issues.** The
619
+ truncation check read `pageInfo.hasNextPage` off the connection _after_ collecting from it, and
594
620
  `fetchNext()` mutates that connection in place — so the flag described the last page fetched
595
621
  rather than the issues the limit hid. With 180 issues at `--limit 150` it returned 150 and said
596
622
  nothing was hidden, suppressing the `… more (use --all)` notice at exactly the moment it was
597
623
  needed. Truncation is now a fact rather than an inference: one extra item is requested (in the
598
624
  same page, so it costs no round-trip) and its presence is the answer. The unit test that shipped
599
- alongside the bug passed because its mock's `fetchNext()` returned a *fresh object* instead of
625
+ alongside the bug passed because its mock's `fetchNext()` returned a _fresh object_ instead of
600
626
  mutating — every faked connection in the suite now comes from one faithful builder
601
627
  (`test/unit/_fakes.ts`) that appends in place and returns `this`, as the SDK does.
602
628
 
603
629
  - **Name resolution stopped at a fixed page, so a large workspace got false `not_found`s.** The
604
630
  resolvers that match client-side asked for a fixed `first: 100`/`first: 250` and searched only
605
631
  what came back: a team, workflow state, cycle or milestone that existed past the cap could not be
606
- resolved by name, and — worse, because it is silent — the *ambiguity* check only ever saw a prefix,
632
+ resolved by name, and — worse, because it is silent — the _ambiguity_ check only ever saw a prefix,
607
633
  so a duplicate name past the cap was invisible and the CLI picked one arbitrarily. Resolution now
608
634
  follows the connection. The page size is 250 (Linear's maximum), so the ordinary workspace still
609
635
  costs the single request it always did and only workspaces that really have more than 250 of
@@ -617,7 +643,7 @@ All notable changes to this project are documented here. The format is based on
617
643
  `--status` is accepted as an alias for the same thing.
618
644
  - **`issue list --label` was case-sensitive.** The filter used an exact-case `in` comparator, so
619
645
  `--label bug` returned an empty list when the label is stored as `Bug` — wrong results with no
620
- error, while label *resolution* everywhere else matches case-insensitively. (Repeating the flag
646
+ error, while label _resolution_ everywhere else matches case-insensitively. (Repeating the flag
621
647
  now narrows the match — see Changed.)
622
648
  - **Deactivated users were invisible.** `team members` and `user list` never sent
623
649
  `includeDisabled`, which Linear defaults to `false` — so deactivated users were never returned
@@ -650,7 +676,7 @@ All notable changes to this project are documented here. The format is based on
650
676
  issue id inferred from the current branch as usual. `issue describe [id]` prints the issue
651
677
  title plus a commit-message trailer using Linear's git magic words (`Fixes <ID>`, or
652
678
  `References <ID>` with `-r`/`--references`) — drop it into `git commit -m "$(linear issue
653
- describe)"`. `issue pull-request [id]` (alias `pr`) creates a GitHub PR via the `gh` CLI:
679
+ describe)"`. `issue pull-request [id]` (alias `pr`) creates a GitHub PR via the `gh` CLI:
654
680
  the title defaults to the issue title (`--title` to override) and the body is the issue
655
681
  description followed by a `Fixes <ID>` trailer and the Linear URL, so the PR and issue
656
682
  reference each other. Flags: `--base`, `--head`, `--draft`, `--web`. The created PR URL is the
@@ -659,8 +685,8 @@ All notable changes to this project are documented here. The format is based on
659
685
  or when `gh` itself fails.
660
686
  - **Multi-workspace credentials.** Store API keys for several workspaces and switch between
661
687
  them. New `auth` subcommands: `auth list` (configured workspaces + default), `auth default
662
- <slug>` (set the default), and `auth token` (print the resolved key for scripting). `auth
663
- login` now accepts the global `--workspace <slug>` (derived from the key's organization when
688
+ <slug>` (set the default), and `auth token` (print the resolved key for scripting). `auth
689
+ login` now accepts the global `--workspace <slug>` (derived from the key's organization when
664
690
  omitted) and `auth status` reports the active credential workspace. A new global
665
691
  `--workspace <slug>` selects which stored credential to use for any command. Credentials live
666
692
  in quoted `[workspaces."<slug>"]` tables under a top-level `default_workspace`. Credential
@@ -668,12 +694,12 @@ All notable changes to this project are documented here. The format is based on
668
694
  `--workspace`/`LINEAR_WORKSPACE`/`default_workspace`, and is never steered by project
669
695
  `.linear.toml`. When several workspaces are configured with no default, the error is deferred
670
696
  until a command actually needs the API — so `auth list`/`default`/`login` still work.
671
- - **`--no-input` did nothing** (AUDIT.md #3). It is the flag that makes the CLI safe to run
697
+ - **`--no-input` did nothing.** It is the flag that makes the CLI safe to run
672
698
  unattended, and it had never once had an effect. Two independent faults, either of which alone
673
699
  was fatal. First, the key: commander stores a negation under the name with `no-` stripped, so the
674
700
  parser wrote `input: false` while `Context` read `noInput` — a field nothing ever set, so the
675
701
  guard was reading `undefined` forever. Second, and the reason fixing the key would not have been
676
- enough: a lone negation is *also* seeded with a default of `true` on every command it is
702
+ enough: a lone negation is _also_ seeded with a default of `true` on every command it is
677
703
  registered on, including the root program, and `optsWithGlobals()` lets ancestors overwrite
678
704
  descendants. Because global options are usable in any position, the flag is parsed by the
679
705
  subcommand — so the root's default `true` then overwrote the subcommand's `false` on the way into
@@ -686,25 +712,25 @@ All notable changes to this project are documented here. The format is based on
686
712
  goes for a redirected stdout, which is where inquirer draws the prompt and would therefore write
687
713
  the question into the caller's output. Commands that need a missing value now fail with the usual
688
714
  usage error naming the flag to pass, instead of waiting forever.
689
- - **`--no-color` broke `label create`** (AUDIT.md #4). The global terminal-colour flag and the
715
+ - **`--no-color` broke `label create`.** The global terminal-colour flag and the
690
716
  entity flag `--color <hex>` both reduced to commander's `color` attribute, so
691
717
  `label create --name x --team TES --no-color` put `color: false` into the mutation input and
692
718
  Linear rejected the call outright: `Variable "$input" got invalid value false at "input.color"`.
693
719
  `project` and `roadmap` escaped the crash only because their guards happened to be truthy-based —
694
720
  they dropped the flag instead. Beyond the crash, there was simply no way to set an entity colour
695
- *and* turn off terminal colour in one command, because one flag overwrote the other. The global
721
+ _and_ turn off terminal colour in one command, because one flag overwrote the other. The global
696
722
  is now spelled **`--no-ansi`**, with `--no-color` kept as an alias on every command (it is the
697
723
  conventional spelling, and both write the same key, so they cannot drift). Terminal colour and
698
724
  entity colour are separate keys now, which makes the whole class of collision structurally
699
725
  impossible rather than fixed one command at a time — a test walks the command tree and asserts no
700
726
  option anywhere can write `false` into `color`.
701
- - **`auth login` echoed the API key** (AUDIT.md #9). The prompt used inquirer's `input`, so the
727
+ - **`auth login` echoed the API key.** The prompt used inquirer's `input`, so the
702
728
  credential appeared on screen as it was typed and then stayed in the terminal's scrollback for as
703
729
  long as the window lived. It now uses inquirer's `password` prompt, which never renders the value.
704
730
  `--key` is unchanged for scripts, and nothing in the command logs or echoes the key — the receipt
705
731
  names the user and the file it was written to.
706
732
  - **`--json --debug` produced output that could not be parsed.** The debug detail was appended
707
- *after* the error envelope as a second, plaintext block, so the one combination a caller reaches
733
+ _after_ the error envelope as a second, plaintext block, so the one combination a caller reaches
708
734
  for when a scripted call misbehaves was the one that broke the contract:
709
735
  `linear … --json --debug 2>&1 | jq` died with "Invalid numeric literal". In JSON mode the detail
710
736
  now lives inside the envelope as `error.detail`. Without `--debug`, or when the error carries no
@@ -724,15 +750,15 @@ All notable changes to this project are documented here. The format is based on
724
750
  different request from the one that was typed, executed without comment. A flag value must now be
725
751
  a complete integer. Priority additionally validates the 0–4 range locally, matching the wording
726
752
  `initiative` has used all along, so the two cannot read as different rules.
727
- - **`project update --team` was accepted and ignored** (AUDIT.md #8). It is rejected now, rather
728
- than implemented: a project belongs to *several* teams, and the existing `--teams` **replaces**
753
+ - **`project update --team` was accepted and ignored.** It is rejected now, rather
754
+ than implemented: a project belongs to _several_ teams, and the existing `--teams` **replaces**
729
755
  that whole set — so quietly treating `--team TES` as `--teams TES` would remove every other team
730
756
  from the project, which is a destructive reading of a flag the user most likely meant as "also
731
757
  this team". The error names `--teams` and says that it replaces. The flag is declared locally on
732
758
  the command (which is what stops the global from being injected over it) and hidden, so `--help`
733
759
  and `linear commands --json` advertise only the flag that works. `project create --team` is
734
760
  untouched — there it is the genuine fallback team.
735
- - **`api --operation` did nothing at all** (AUDIT.md #7). It was passed as a fourth argument to the
761
+ - **`api --operation` did nothing at all.** It was passed as a fourth argument to the
736
762
  SDK's `rawRequest`, which takes three — so it was discarded, and a multi-operation document went
737
763
  to the API with no operation named. The API's answer to that is not "run the first one" but a
738
764
  flat `The operation does not exist on the query.`, which made multi-operation documents unusable
@@ -745,12 +771,12 @@ All notable changes to this project are documented here. The format is based on
745
771
  multi-operation document is now a usage error that lists the names, rather than a confusing error
746
772
  from the server, and `--operation` naming an operation the document does not define is caught here
747
773
  too.
748
- - **`api --paginate` never checked what it was re-running** (AUDIT.md #10). The loop re-executes the
749
- whole document once per cursor, so a *mutation* whose payload happens to contain a paginatable
774
+ - **`api --paginate` never checked what it was re-running.** The loop re-executes the
775
+ whole document once per cursor, so a _mutation_ whose payload happens to contain a paginatable
750
776
  connection would have been executed once per page — creating duplicate entities, silently, in
751
777
  proportion to how much data came back. It now refuses anything that is not a query, and refuses it
752
778
  **before the first request**, so the check cannot itself cost a side effect. The kind is read from
753
- the same parse that `--operation` uses, which means it is the *selected* operation that is
779
+ the same parse that `--operation` uses, which means it is the _selected_ operation that is
754
780
  checked, not merely the first one in the file.
755
781
  - **`schema --json --output <file>` ignored the file.** The JSON branch returned before reaching the
756
782
  write, so the introspection result went to stdout and no file was ever created — silently, since
@@ -764,7 +790,7 @@ All notable changes to this project are documented here. The format is based on
764
790
  stderr, where it lands in scrollback, CI logs and bug reports. The same channel let a project
765
791
  `.linear.toml` — a file that is not ours and may arrive with a checkout — emit raw escape
766
792
  sequences through our error output. We now report only the reason and the position (`line 3,
767
- column 36`), never an excerpt from a credential-bearing file, and strip control characters and
793
+ column 36`), never an excerpt from a credential-bearing file, and strip control characters and
768
794
  bidi overrides from what remains. The error stays actionable: it still says exactly where to look.
769
795
  - **Credential writes could lose a config or leave a truncated one.** Writing the config in place
770
796
  truncates it first, so a crash or a concurrently-running `linear` could observe — or leave behind
package/COVERAGE.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  Generated by `bun run audit:coverage`. Total LinearClient members: **470**.
4
4
 
5
- | Status | Count | Meaning |
6
- | --- | --- | --- |
7
- | curated | 102 | Has a first-class CLI command |
8
- | raw-only | 140 | Reachable via `linear api` (no bespoke command) |
9
- | excluded | 228 | Out of scope (admin/integration/SDK plumbing) |
5
+ | Status | Count | Meaning |
6
+ | -------- | ----- | ----------------------------------------------- |
7
+ | curated | 102 | Has a first-class CLI command |
8
+ | raw-only | 140 | Reachable via `linear api` (no bespoke command) |
9
+ | excluded | 228 | Out of scope (admin/integration/SDK plumbing) |
10
10
 
11
11
  Every member is classified — CI fails otherwise. `raw-only` members are still
12
12
  100% reachable through the raw GraphQL escape hatch; they simply lack a tailored command.