linear-sdk-cli 0.1.3 → 0.2.3

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 +96 -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 +97 -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
package/MIGRATING.md CHANGED
@@ -27,7 +27,7 @@ things make it safe anyway:
27
27
  deno install -A --reload -f -g -n linear-schpet jsr:@schpet/linear-cli && deno uninstall -g linear # deno
28
28
  brew unlink schpet/tap/linear && ln -s "$(brew --prefix)/opt/linear/bin/linear" ~/.local/bin/linear-schpet # homebrew
29
29
  mv "$(command -v linear)" "$(dirname "$(command -v linear)")/linear-schpet" # npm/bun -g
30
- bun add -g linear-sdk-cli # or: bun link, from a clone
30
+ bun add --global linear-sdk-cli
31
31
  linear --version # ours: 0.x
32
32
  ```
33
33
 
@@ -60,7 +60,7 @@ We look in every place schpet does, in its order. Walking up from the working di
60
60
  directory is checked for `linear.toml`, then `.linear.toml`, then `.config/linear.toml` (schpet
61
61
  checks cwd and the git root; we check every directory between, which agrees with it wherever it
62
62
  would find something). Below that, our `~/.config/linear/config.toml`, then schpet's global
63
- `~/.config/linear/linear.toml` — read for `team_id`, `workspace`, `issue_sort`, `vcs`; **not** for
63
+ `~/.config/linear/linear.toml` — read for `team_id`, `workspace`, `issue_sort`; **not** for
64
64
  `api_key`, which schpet allows there and we never take from any file but our own. `linear config`
65
65
  prints each value with the file it came from, so nothing is a mystery:
66
66
 
@@ -70,90 +70,91 @@ Sort: priority (global: /Users/you/.config/linear/linear.toml)
70
70
  ```
71
71
 
72
72
  Two key differences to know: `issue_sort = "manual"` (schpet's board order) has no equivalent here
73
- and is a usage error naming the file — change it to `priority` or drop the line; and schpet's
73
+ and is a usage error naming the file — change it to `priority` or drop the line. The `vcs` setting
74
+ is ignored; this CLI supports Git and does not claim partial jj support. Schpet's
74
75
  `issue_create_*`, `download_images`, `hyperlink_format`, `attachment_dir` keys are ignored (no
75
76
  such features yet). `linear config init` writes a `.linear.toml`; `linear config set <key>
76
77
  <value>` edits one key.
77
78
 
78
79
  ## 4. Commands — same words, or an alias
79
80
 
80
- | schpet | here | notes |
81
- |---|---|---|
82
- | `issue list` | **`issue mine`** | ⚠️ their `list` is an alias of `mine` (you, unstarted). Our `list` is the whole team. See §6. |
83
- | `issue query` | `issue list` | alias, same command |
84
- | `issue mine` | `issue mine` | same defaults: yours, unstarted; `--all-states` widens |
85
- | `issue comment add\|list\|update\|delete` | same, or top-level `comment …` | one implementation |
86
- | `issue comment <id> "<body>"` | same | |
87
- | `issue attach <file>` | `issue attach <issue> <file...>` | same posture: private by default, `--public` for raster images only; ours takes several files and has `--json` |
88
- | `issue link <url>` | `attachment create --url` | |
89
- | `issue comment add --attach <file>` | same, or `comment add --attach` | repeatable; images render inline |
90
- | `auth whoami` | `whoami` (also `auth whoami`) | |
91
- | `auth migrate` | `auth migrate` | |
92
- | `config` (writes toml) | `config init` / `config set` | |
93
- | `team states` / `team members` / `user list` | same | `--all` there means "inactive too"; here it is pagination — use `--include-disabled` (see §6) |
94
- | `issue start` | same | moves the issue to the first `started` state, as there; `--no-move` to only branch |
95
- | `issue describe` | same | byte-identical output: `ID Title`, then `Linear-issue:` / `Linear-issue-url:` trailers |
96
- | `issue pull-request` | same | title `ID Title` as there; body is the two `Linear-issue` trailers (theirs: the URL alone) — the issue description is not copied into GitHub |
97
- | `project create -t A -t B` | same, or `--teams A,B` | collects both, as there (see §6) |
98
- | `document create\|list\|update --project\|--issue\|--initiative\|--team\|--cycle\|--release` | same | `update` re-points, as there; `--team` is the global flag (with `--cycle` it scopes the lookup) |
99
- | `initiative add-project` / `remove-project` / `unarchive` | same | |
100
- | `project delete` / `team delete` | same, confirmation-gated | |
101
- | `schema` / `api` | same | ours adds `--operation`, `--vars-file`, and refuses to `--paginate` a mutation |
81
+ | schpet | here | notes |
82
+ | -------------------------------------------------------------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
83
+ | `issue list` | **`issue mine`** | ⚠️ their `list` is an alias of `mine` (you, unstarted). Our `list` is the whole team. See §6. |
84
+ | `issue query` | `issue list` | alias, same command |
85
+ | `issue mine` | `issue mine` | same defaults: yours, unstarted; `--all-states` widens |
86
+ | `issue comment add\|list\|update\|delete` | same, or top-level `comment …` | one implementation |
87
+ | `issue comment <id> "<body>"` | same | |
88
+ | `issue attach <file>` | `issue attach <issue> <file...>` | same posture: private by default, `--public` for raster images only; ours takes several files and has `--json` |
89
+ | `issue link <url>` | `attachment create --url` | |
90
+ | `issue comment add --attach <file>` | same, or `comment add --attach` | repeatable; images render inline |
91
+ | `auth whoami` | `whoami` (also `auth whoami`) | |
92
+ | `auth migrate` | `auth migrate` | |
93
+ | `config` (writes toml) | `config init` / `config set` | |
94
+ | `team states` / `team members` / `user list` | same | `--all` there means "inactive too"; here it is pagination — use `--include-disabled` (see §6) |
95
+ | `issue start` | same | moves the issue to the first `started` state, as there; `--no-move` to only branch |
96
+ | `issue describe` | same | byte-identical output: `ID Title`, then `Linear-issue:` / `Linear-issue-url:` trailers |
97
+ | `issue pull-request` | same | title `ID Title` as there; body is the two `Linear-issue` trailers (theirs: the URL alone) — the issue description is not copied into GitHub |
98
+ | `project create -t A -t B` | same, or `--teams A,B` | collects both, as there (see §6) |
99
+ | `document create\|list\|update --project\|--issue\|--initiative\|--team\|--cycle\|--release` | same | `update` re-points, as there; `--team` is the global flag (with `--cycle` it scopes the lookup) |
100
+ | `initiative add-project` / `remove-project` / `unarchive` | same | |
101
+ | `project delete` / `team delete` | same, confirmation-gated | |
102
+ | `schema` / `api` | same | ours adds `--operation`, `--vars-file`, and refuses to `--paginate` a mutation |
102
103
 
103
104
  Everything with no row is spelled identically.
104
105
 
105
106
  ## 5. Flags — theirs are accepted
106
107
 
107
- | schpet | here (canonical) | where |
108
- |---|---|---|
109
- | `-j, --json` | same | every command |
110
- | `-w, --web` | same | every `view` |
111
- | `--due-date` | `--due` | issue create/update |
112
- | `--target-date`, `--start-date` | `--target`, `--start` | project / milestone / initiative |
113
- | `--search` | `--query` | issue list / mine |
114
- | `--status` | `--state` | project list |
115
- | `--limit 0` | `--all` | every list |
116
- | `--assignee self` | `me` / `@me` | anywhere |
117
- | `--cycle active` | `current` | anywhere; `now`/`next`/`previous`/`+1` also work |
118
- | `-U/--unassigned`, `--created-after`, `--updated-after`, `--project-label`, `--milestone`, `--search-comments`, repeatable `--team`/`--state` | same | issue queries |
119
- | `--add-label`, `--remove-label`, `--unassign`, `--clear-cycle` | same | issue update |
120
- | `--no-use-default-template` | `--no-default-template` | issue create — the team default is applied unless you say so, as there |
121
- | `--start`, `--parent` (child joins the parent's project) | same | issue create |
122
-
123
- Passing both spellings at once (`--due` *and* `--due-date`) is a usage error, not a coin flip.
108
+ | schpet | here (canonical) | where |
109
+ | --------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ---------------------------------------------------------------------- |
110
+ | `-j, --json` | same | every command |
111
+ | `-w, --web` | same | every `view` |
112
+ | `--due-date` | `--due` | issue create/update |
113
+ | `--target-date`, `--start-date` | `--target`, `--start` | project / milestone / initiative |
114
+ | `--search` | `--query` | issue list / mine |
115
+ | `--status` | `--state` | project list |
116
+ | `--limit 0` | `--all` | every list |
117
+ | `--assignee self` | `me` / `@me` | anywhere |
118
+ | `--cycle active` | `current` | anywhere; `now`/`next`/`previous`/`+1` also work |
119
+ | `-U/--unassigned`, `--created-after`, `--updated-after`, `--project-label`, `--milestone`, `--search-comments`, repeatable `--team`/`--state` | same | issue queries |
120
+ | `--add-label`, `--remove-label`, `--unassign`, `--clear-cycle` | same | issue update |
121
+ | `--no-use-default-template` | `--no-default-template` | issue create — the team default is applied unless you say so, as there |
122
+ | `--start`, `--parent` (child joins the parent's project) | same | issue create |
123
+
124
+ Passing both spellings at once (`--due` _and_ `--due-date`) is a usage error, not a coin flip.
124
125
 
125
126
  **Short flags are the one place we did not follow.** schpet 2.5's own tree assigns `-a` four
126
127
  meanings (`--app`, `--all`, `--assignee`, `--attach`), `-f` four, `-y` three, `-t` two (`--title`
127
128
  and `--team`). There is no consistent target to copy. Ours holds **one meaning per letter across
128
129
  all 141 commands**, so `-t` is always `--team`, `-p` always `--project`, `-P` always `--priority`,
129
130
  `-f` always `--fields`, `-n` always `--limit`. Every collision fails loudly — `-p 2` says "No
130
- project matching '2'", not "set priority 2" — and the two that *used* not to are loud now: `-f`
131
+ project matching '2'", not "set priority 2" — and the two that _used_ not to are loud now: `-f`
131
132
  and `-n` are refused on any command that prints no table (`project create -f desc.md` says
132
133
  "--fields does not apply … use --description-file <path>"; `project create -n 'My Project'` says
133
134
  "--limit does not apply … the name is --name"), instead of creating the project without either.
134
135
 
135
136
  ## 6. What would silently differ — and how we made it loud (or the same)
136
137
 
137
- These are the spots where the same command line could *succeed on both and mean different things*.
138
+ These are the spots where the same command line could _succeed on both and mean different things_.
138
139
  Every one is now either identical, or says so on stderr, or is a usage error. Know them.
139
140
 
140
- 1. **`issue list`** — theirs shows *your unstarted* issues; ours shows the *team's* issues.
141
+ 1. **`issue list`** — theirs shows _your unstarted_ issues; ours shows the _team's_ issues.
141
142
  We did not alias `list` to `mine`: a command named "list" that hides your colleagues' work and
142
143
  your own in-progress work is the sharpest transition hazard there is. Type `issue mine` for the
143
144
  old behavior.
144
145
  2. **Repeated `--label`** narrows (AND) in both CLIs now — same as schpet.
145
146
  3. **`--sort priority`** groups by workflow state in both — but we sort state **ascending** so active
146
- work is on top. schpet hardcodes descending, which the API answers with Backlog *above* In
147
+ work is on top. schpet hardcodes descending, which the API answers with Backlog _above_ In
147
148
  Progress; a Low-priority backlog item outranks an Urgent in-progress one there. We diverge on
148
149
  purpose.
149
- 4. **`user list --all` / `team members --all`** — theirs: include *inactive* members. Ours: `--all`
150
+ 4. **`user list --all` / `team members --all`** — theirs: include _inactive_ members. Ours: `--all`
150
151
  is the global "fetch every page" on all 141 commands, and deactivated users are
151
152
  `--include-disabled`. We kept the one meaning; the command prints a warning on stderr (even
152
153
  under `--quiet`) that deactivated users are still excluded and names `--include-disabled`, and
153
154
  `--help` says so up front. Not adopted: making `--all` mean "and deactivated" on two commands
154
155
  would give one global two meanings.
155
156
  5. **`-f <file>` on `project create/update`, `document create/update`** — theirs: `--description-file` /
156
- `--content-file`. Ours: `-f` is `--fields`, and it used to be *dropped* on those commands, so
157
+ `--content-file`. Ours: `-f` is `--fields`, and it used to be _dropped_ on those commands, so
157
158
  `project create --name X -f desc.md` created the project with no description and exited 0.
158
159
  `--fields`, `--limit` and `--all` are now usage errors on every command that prints no table or
159
160
  detail block, and the message names `--description-file` / `--content-file` where they exist.
@@ -162,7 +163,7 @@ Every one is now either identical, or says so on stderr, or is a usage error. Kn
162
163
  6. **`project create --team A --team B`** — theirs collects both; ours took the last one and created
163
164
  the project in B alone. `--team` on `project create` is now the same repeatable list as
164
165
  `--teams`; both spellings at once is a usage error. (`project update --team` stays refused,
165
- pointing at `--teams`, because that list *replaces* the project's teams.)
166
+ pointing at `--teams`, because that list _replaces_ the project's teams.)
166
167
  7. **`issue start`** — theirs always moves the issue to the first `started` state after branching;
167
168
  ours only did with `--move`, so the transplanted command left the issue in Backlog without a
168
169
  word. Adopted: moving is the default (that is what "start" means, and what an agent that says
@@ -171,36 +172,37 @@ Every one is now either identical, or says so on stderr, or is a usage error. Kn
171
172
  8. **`issue describe`** — theirs prints `ID Title`, a blank line, then `Linear-issue: Fixes ID` and
172
173
  `Linear-issue-url: URL`; ours printed `Title` and a bare `Fixes ID`. Piped into `git commit -m`
173
174
  that was a different commit. Adopted, byte for byte: proper trailers are what `git
174
- interpret-trailers` and jj read back, and Linear reads the magic word right before the id
175
+ interpret-trailers` and jj read back, and Linear reads the magic word right before the id
175
176
  either way (linear.app/docs/github). `-r/--references` still swaps in `References`.
176
177
  9. **`issue pull-request`** — theirs titles the PR `ID Title` and sends the issue URL as the body;
177
- ours titled it `Title` and pasted the issue *description* into the body. Title adopted (a custom
178
+ ours titled it `Title` and pasted the issue _description_ into the body. Title adopted (a custom
178
179
  `--title` is prefixed too). Body: the same two trailers `describe` prints — the URL as there,
179
180
  plus the magic word so the link (and auto-close) does not depend on the branch name — and the
180
181
  description stays in Linear rather than in a GitHub PR body.
181
182
  10. **`issue list`/`mine`/`search`, `project list` with no team configured** — theirs error
182
- ("No default team…"); ours list the whole workspace, on purpose (`--all-teams` explicitly). So a
183
- migrating user cannot mistake that for a team's list, the command prints one stderr note ("No
184
- default team configured; listing every team's…"); `--quiet` silences it.
185
- 11. **`--cycle +1`** — active cycle plus one on both (it was cycle *#1* here once).
183
+ ("No default team…"); ours list the whole workspace, on purpose (`--all-teams` explicitly). So a
184
+ migrating user cannot mistake that for a team's list, the command prints one stderr note ("No
185
+ default team configured; listing every team's…"); `--quiet` silences it.
186
+ 11. **`--cycle +1`** — active cycle plus one on both (it was cycle _#1_ here once).
186
187
  12. **`document list --team X`** — filters to the team's documents on both.
187
188
 
188
189
  Loud already, listed so you are not surprised:
189
190
 
190
- | you type (schpet habit) | here | what happens |
191
- |---|---|---|
192
- | `issue update TES-1 -t 'New title'` (`-t` = `--title` there) | `--title` | `-t` is `--team`: "No team matching 'New title'", exit 3 — unless a team is really named that |
193
- | `project create -n 'My Project'` (`-n` = `--name` there) | `--name` | `-n` is `--limit`: refused on `create`, and the message says so |
194
- | `issue comment delete <id>` in a script | same, `--yes` | destructive actions off-TTY need `--yes` (exit 2), there it just deletes |
195
- | `--assignee <partial name>` (fuzzy there) | `me`, an email, an exact display/full name | "No user matching", exit 3 |
196
- | `issue view 123` (bare number, team from config there) | `TES-123` | rejected, exit 2 |
197
- | `linear config` (writes a toml there) | prints; `config init` / `config set` write | |
198
- | `project update <p> -t A` (replaces the teams there) | `--teams A` | `--team` refused with the reason; `--teams` replaces, as there |
199
- | `--all` on `user list` | see 4 above | pagination + a warning |
200
-
201
- ## 7. Not here yet
202
-
203
- `issue commits`, `team autolinks`, jj support, bulk `--bulk-file`, markdown rendering + pager.
191
+ | you type (schpet habit) | here | what happens |
192
+ | ------------------------------------------------------------ | ------------------------------------------ | --------------------------------------------------------------------------------------------- |
193
+ | `issue update TES-1 -t 'New title'` (`-t` = `--title` there) | `--title` | `-t` is `--team`: "No team matching 'New title'", exit 3 — unless a team is really named that |
194
+ | `project create -n 'My Project'` (`-n` = `--name` there) | `--name` | `-n` is `--limit`: refused on `create`, and the message says so |
195
+ | `issue comment delete <id>` in a script | same, `--yes` | destructive actions off-TTY need `--yes` (exit 2), there it just deletes |
196
+ | `--assignee <partial name>` (fuzzy there) | `me`, an email, an exact display/full name | "No user matching", exit 3 |
197
+ | `issue view 123` (bare number, team from config there) | `TES-123` | rejected, exit 2 |
198
+ | `linear config` (writes a toml there) | prints; `config init` / `config set` write | |
199
+ | `project update <p> -t A` (replaces the teams there) | `--teams A` | `--team` refused with the reason; `--teams` replaces, as there |
200
+ | `--all` on `user list` | see 4 above | pagination + a warning |
201
+
202
+ ## 7. Deliberate non-features
203
+
204
+ `issue commits`, `team autolinks`, and jj support are not curated commands. Bulk archive/delete
205
+ supports `--bulk`, `--bulk-file`, and `--bulk-stdin`; Markdown rendering and paging are built in.
204
206
  Each is tracked; the raw `linear api` reaches all of the API surface in the meantime. If you type
205
207
  one of these out of habit, the CLI says where the equivalent lives rather than just rejecting it:
206
208
  `linear issue link x` answers "Use 'linear attachment create <issue> --url <url>'", and `issue
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  **An ergonomic command-line interface for [Linear](https://linear.app), built on the official
4
4
  [`@linear/sdk`](https://www.npmjs.com/package/@linear/sdk).**
5
5
 
6
- It's designed to be pleasant for humans *and* dependable for scripts and agents. By default you
6
+ It's designed to be pleasant for humans _and_ dependable for scripts and agents. By default you
7
7
  get clean, aligned tables and detail views; add `--json` to any command for a stable,
8
8
  machine-readable shape. It's git-aware (the "current issue" comes from your branch name) and
9
9
  forgiving about input (`--assignee me`, team key `TES`, state and label by name). Anything the
@@ -21,7 +21,8 @@ linear issue list --json | jq -r '.[].identifier' # ready for scripts
21
21
 
22
22
  ## Highlights
23
23
 
24
- - **Human-first by default** — aligned tables and readable detail views, color-aware, paged sanely.
24
+ - **Human-first by default** — aligned tables, rendered Markdown, safe paging for long detail,
25
+ color controls, and a clear notice when a list was truncated.
25
26
  - **Agent- and script-friendly** — every data command takes `--json` and emits a stable, documented envelope on stdout; status text stays on stderr.
26
27
  - **Git-aware** — the current issue is inferred from your branch (`tes-123-fix` → `TES-123`), so most issue commands let you drop the id.
27
28
  - **Git + GitHub workflow** — `issue start` checks out the branch and marks the issue started, `issue describe` prints a commit message with Linear-issue trailers, `issue pr` opens a GitHub PR — all linked back to the issue.
@@ -36,8 +37,8 @@ Requires [Bun](https://bun.sh) **1.1 or newer** and a Linear API key
36
37
  directly on Bun — no build step, no bundle, no Node.
37
38
 
38
39
  ```sh
39
- bun add -g linear-sdk-cli # or, for a one-off: bunx linear-sdk-cli --help
40
- linear --help
40
+ bun add --global linear-sdk-cli
41
+ lin --help
41
42
  ```
42
43
 
43
44
  This installs two equivalent binaries: **`linear`** and the shorter **`lin`**. If you already
@@ -65,6 +66,7 @@ the other is silently shadowed — `linear --version` tells you which you got (`
65
66
  ```
66
67
 
67
68
  (A package-manager upgrade of theirs may put `linear` back; re-run the line for your channel.)
69
+
68
70
  - **Uninstall theirs** once you no longer need it. Your credentials survive: they are in the OS
69
71
  keyring under the same service and account, and we read them — see [Authentication](#authentication).
70
72
 
@@ -76,10 +78,11 @@ collide with a global install of ours; inside such a project `bunx linear` is th
76
78
  <summary>Run from source instead</summary>
77
79
 
78
80
  ```sh
79
- git clone <this-repo> && cd linear-sdk-cli
81
+ git clone https://github.com/zhendalf/linear-sdk-cli.git && cd linear-sdk-cli
80
82
  bun install
81
83
  bun run src/bin/linear.ts --help # or: bun run dev -- --help
82
84
  ```
85
+
83
86
  </details>
84
87
 
85
88
  ## Quickstart
@@ -154,7 +157,7 @@ A few ideas run through every command:
154
157
  `issue view TES-123`), and nearly every issue subcommand infers the id from the branch — so
155
158
  `linear issue comment "…"`, `linear issue start`, `linear issue pr` all "just work" in context.
156
159
  - **Human by default, `--json` for machines.** Without `--json` you get tables and detail views
157
- meant to be read. With `--json`, stdout carries *only* machine JSON (a [stable
160
+ meant to be read. With `--json`, stdout carries _only_ machine JSON (a [stable
158
161
  envelope](#scripting--agents)); status and progress always go to stderr.
159
162
  - **Forgiving inputs.** Resolve things by how you think about them: `--assignee me`, assignee by
160
163
  email or name, team key `TES`, `--cycle current`, workflow state and label by name (case-
@@ -195,6 +198,19 @@ so the PR and issue reference each other; the issue description stays in Linear.
195
198
  or creates branches for you, and fails with a clear error (not a stack trace) when `gh` is
196
199
  missing, unauthenticated, or the branch isn't pushed.
197
200
 
201
+ **Comments & explicit mentions** — ordinary `@name` text stays literal and never notifies anyone.
202
+ Use repeatable `--mention <name|email|me|id>` when a real Linear mention is intentional; the CLI
203
+ resolves each user exactly, deduplicates repeats, and prepends the mentions as their own Markdown
204
+ paragraph. The flag works on `issue comment`, `comment add`, `comment reply`, and `comment update`
205
+ (including the `issue comment add/update` aliases).
206
+
207
+ ```sh
208
+ linear issue comment TES-123 "Please review this." --mention ada
209
+ linear comment add TES-123 --mention ada --mention grace --body-file - <<'EOF'
210
+ The migration plan is ready for review.
211
+ EOF
212
+ ```
213
+
198
214
  **Projects & status updates**
199
215
 
200
216
  ```sh
@@ -211,55 +227,55 @@ Status updates (`project-update`/`pu`, `initiative-update`/`iu`) take the body f
211
227
  ## Command overview
212
228
 
213
229
  Every group has `--help` with full options and (for the busy ones) an Examples section. Aliases
214
- are shown in parentheses. For a machine-readable tree of *every* command, run
230
+ are shown in parentheses. For a machine-readable tree of _every_ command, run
215
231
  `linear commands --json`.
216
232
 
217
- | Group | What you can do |
218
- | --- | --- |
219
- | **`issue`** (`i`) | `view` · `list` · `mine` · `search` · `create` · `update` · `delete` · `archive`/`unarchive` · `start` (git branch) · `describe` · `pull-request`/`pr` · `assign` · `state` · `label` · `comment`/`comments` · `relation` · `subscribe`/`unsubscribe` · `id`/`title`/`url`/`branch` |
220
- | **`team`** (`t`) | `list` · `view` · `members` · `states` · `labels` · `cycles` · `create` · `update` |
221
- | **`project`** (`p`) | `list` · `view` · `create` · `update` · `archive` · `milestones` |
222
- | **`project-update`** (`pu`) | `create` · `list` (project status updates, with `--health`) |
223
- | **`milestone`** (`m`) | `list` · `view` · `create` · `update` · `delete` |
224
- | **`cycle`** (`c`) | `list` · `view` · `current` · `create` · `update` |
225
- | **`user`** (`u`) | `list` · `view` · `me` |
226
- | **`label`** (`lb`) | `list` · `create` · `update` · `delete` |
227
- | **`state`** (`st`) | `list` · `view` (workflow states) |
228
- | **`comment`** (`cm`) | `list` · `add` · `reply` · `update` · `delete` · `resolve`/`unresolve` |
229
- | **`document`** (`doc`) | `list` · `view` · `create` · `update` · `delete` |
230
- | **`attachment`** (`at`) | `list` · `create` · `delete` |
231
- | **`favorite`** (`fav`) | `list` · `add` · `remove` |
232
- | **`initiative`** (`init`) | `list` · `view` · `create` · `update` · `archive` · `delete` |
233
- | **`initiative-update`** (`iu`) | `create` · `list` (initiative status updates, with `--health`) |
234
- | **`roadmap`** (`rm`) | `list` · `view` · `create` · `update` · `delete` &nbsp;<sup>†</sup> |
235
- | **`notification`** (`notif`) | `list` · `read`/`unread` · `read-all` · `archive` · `snooze` |
236
- | **`organization`** (`org`) | `view` · `members` · `invites` |
237
- | **`webhook`** (`wh`) | `list` · `view` · `create` · `update` · `delete` |
238
- | **top-level** | `whoami` · `auth` (`login` · `list` · `default` · `token` · `status` · `logout`) · `config` · `api` · `commands` · `schema` · `completion` |
233
+ | Group | What you can do |
234
+ | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
235
+ | **`issue`** (`i`) | `view` · `list` · `mine` · `search` · `create` · `update` · `delete` · `archive`/`unarchive` · `start` (git branch) · `describe` · `pull-request`/`pr` · `assign` · `state` · `label` · `comment`/`comments` · `relation` · `subscribe`/`unsubscribe` · `id`/`title`/`url`/`branch` |
236
+ | **`team`** (`t`) | `list` · `view` · `members` · `states` · `labels` · `cycles` · `create` · `update` |
237
+ | **`project`** (`p`) | `list` · `view` · `create` · `update` · `archive` · `milestones` |
238
+ | **`project-update`** (`pu`) | `create` · `list` (project status updates, with `--health`) |
239
+ | **`milestone`** (`m`) | `list` · `view` · `create` · `update` · `delete` |
240
+ | **`cycle`** (`c`) | `list` · `view` · `current` · `create` · `update` |
241
+ | **`user`** (`u`) | `list` · `view` · `me` |
242
+ | **`label`** (`lb`) | `list` · `create` · `update` · `delete` |
243
+ | **`state`** (`st`) | `list` · `view` (workflow states) |
244
+ | **`comment`** (`cm`) | `list` · `add` · `reply` · `update` · `delete` · `resolve`/`unresolve` |
245
+ | **`document`** (`doc`) | `list` · `view` · `create` · `update` · `delete` |
246
+ | **`attachment`** (`at`) | `list` · `create` · `delete` |
247
+ | **`favorite`** (`fav`) | `list` · `add` · `remove` |
248
+ | **`initiative`** (`init`) | `list` · `view` · `create` · `update` · `archive` · `delete` |
249
+ | **`initiative-update`** (`iu`) | `create` · `list` (initiative status updates, with `--health`) |
250
+ | **`roadmap`** (`rm`) | `list` · `view` · `create` · `update` · `delete` &nbsp;<sup>†</sup> |
251
+ | **`notification`** (`notif`) | `list` · `read`/`unread` · `read-all` · `archive` · `snooze` |
252
+ | **`organization`** (`org`) | `view` · `members` · `invites` |
253
+ | **`webhook`** (`wh`) | `list` · `view` · `create` · `update` · `delete` |
254
+ | **top-level** | `whoami` · `auth` (`login` · `list` · `default` · `token` · `status` · `logout`) · `config` · `api` · `commands` · `schema` · `completion` |
239
255
 
240
256
  <sup>†</sup> Linear has **deprecated roadmaps** in favor of initiatives — reads still work, but the
241
257
  API rejects roadmap mutations with a deprecation notice. Use `initiative` for new work.
242
258
 
243
259
  ### Global flags
244
260
 
245
- | Flag | Effect |
246
- | --- | --- |
247
- | `--json` | Emit machine JSON only on stdout (see [the contract](#scripting--agents)). |
248
- | `-f, --fields a,b,c` | Choose which columns to show (table output), detail lines, or JSON keys. Refused on a command that prints only a receipt. |
249
- | `-n, --limit <n>` / `--all` | Cap results, or fetch every page. Refused on a command that pages nothing. |
250
- | `-t, --team <key>` | Set the default team for the command. |
251
- | `--workspace <slug>` | Select which stored workspace credential to use. |
252
- | `-y, --yes` | Skip confirmation prompts (required for destructive actions when not a TTY). |
253
- | `--no-input` | Never prompt; fail with a usage error instead of hanging. |
254
- | `--no-ansi` | Disable colored output (`--no-color` is accepted as an alias). |
255
- | `-q, --quiet` · `--debug` | Silence status output · verbose errors. |
261
+ | Flag | Effect |
262
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
263
+ | `--json` | Emit machine JSON only on stdout (see [the contract](#scripting--agents)). |
264
+ | `-f, --fields a,b,c` | Choose which columns to show (table output), detail lines, or JSON keys. Refused on a command that prints only a receipt. |
265
+ | `-n, --limit <n>` / `--all` | Cap results, or fetch every page. Refused on a command that pages nothing. |
266
+ | `-t, --team <key>` | Set the default team for the command. |
267
+ | `--workspace <slug>` | Select which stored workspace credential to use. |
268
+ | `-y, --yes` | Skip confirmation prompts (required for destructive actions when not a TTY). |
269
+ | `--no-input` | Never prompt; fail with a usage error instead of hanging. |
270
+ | `--no-ansi` | Disable colored output (`--no-color` is accepted as an alias). |
271
+ | `-q, --quiet` · `--debug` | Silence status output · verbose errors. |
256
272
 
257
273
  ## Scripting & agents
258
274
 
259
275
  The CLI is built to be driven by scripts and agents. Everything in this section is a stable
260
276
  contract.
261
277
 
262
- **The `--json` envelope.** With `--json`, stdout carries *only* machine JSON, pretty-printed, one
278
+ **The `--json` envelope.** With `--json`, stdout carries _only_ machine JSON, pretty-printed, one
263
279
  value per command:
264
280
 
265
281
  - **list** commands emit a **bare array** (`[...]`) — even when empty (`[]`) and even for a single result.
@@ -268,7 +284,7 @@ value per command:
268
284
  small shape like `{ "id", "identifier", "url" }`, or `{ "id", "success": true }` when the API
269
285
  returns no body. Destructive commands add a flag such as `{ "deleted": true }` / `{ "archived": true }`.
270
286
  - **errors** go to **stderr** as `{"error":{"message":"…","code":"…"}}` and never to stdout. With
271
- `--debug`, the extra detail is carried *inside* that object as `error.detail` rather than appended
287
+ `--debug`, the extra detail is carried _inside_ that object as `error.detail` rather than appended
272
288
  after it, so `--json --debug` output stays parseable.
273
289
 
274
290
  Status, progress, and pagination notes always go to **stderr**, so `cmd --json` is safe to pipe
@@ -282,15 +298,15 @@ ID=$(linear issue create --title "Fix" --team TES --json | jq -r '.id')
282
298
 
283
299
  **Exit codes** are stable and distinct, so a script can branch on the failure class:
284
300
 
285
- | Code | Name | When |
286
- | ---: | --- | --- |
287
- | `0` | ok | success |
288
- | `1` | runtime/API | network/GraphQL/other runtime failure (also feature-not-accessible) |
289
- | `2` | usage | bad flags/arguments, missing required input, validation |
290
- | `3` | not-found/ambiguous | the referenced resource doesn't exist, or a name matched many |
291
- | `4` | auth | missing/invalid API key, or forbidden |
292
- | `5` | rate-limited | Linear rate limit hit |
293
- | `6` | cancelled | a confirmation prompt was declined — nothing was changed |
301
+ | Code | Name | When |
302
+ | ---: | ------------------- | ------------------------------------------------------------------- |
303
+ | `0` | ok | success |
304
+ | `1` | runtime/API | network/GraphQL/other runtime failure (also feature-not-accessible) |
305
+ | `2` | usage | bad flags/arguments, missing required input, validation |
306
+ | `3` | not-found/ambiguous | the referenced resource doesn't exist, or a name matched many |
307
+ | `4` | auth | missing/invalid API key, or forbidden |
308
+ | `5` | rate-limited | Linear rate limit hit |
309
+ | `6` | cancelled | a confirmation prompt was declined — nothing was changed |
294
310
 
295
311
  The error `code` field in the JSON envelope is one of: `usage`, `auth`, `not_found`, `ambiguous`,
296
312
  `forbidden`, `validation`, `rate_limited`, `network`, `feature_not_accessible`, `api`, `runtime`.
@@ -304,7 +320,7 @@ so prefer the `code` field for fine-grained handling and the exit code for coars
304
320
  implies this**, as does a stdout that is not a TTY: a prompt inside a pipeline is a hang, not a
305
321
  question, so you never have to remember both flags.
306
322
  - **`-y, --yes`** — pre-confirm destructive actions (`delete`/`archive`). Without a TTY,
307
- destructive commands *require* `--yes` (they refuse rather than block). If a human declines the
323
+ destructive commands _require_ `--yes` (they refuse rather than block). If a human declines the
308
324
  prompt, the command exits **`6`** and reports `{"cancelled": true, "action": "…"}` under `--json`
309
325
  — never `0`, so `linear issue delete X && …` cannot run the `&&` side after a "no".
310
326
  - **`-q, --quiet`** — suppress success/status lines on stderr (errors still print).
@@ -334,22 +350,32 @@ grep 'type Issue ' /tmp/linear.graphql
334
350
  **File-based bodies & stdin.** Long text (issue descriptions, comments, status updates) can come
335
351
  from a file or stdin instead of a flag — e.g. `--body-file <path>` with `-` for stdin, or
336
352
  `--editor` to open `$EDITOR`. The raw `linear api` reads from `--query-file -` and `--vars-file -`
337
- too, so you can pipe GraphQL straight in.
353
+ too, so you can pipe GraphQL straight in. Inline bodies containing literal `\n` sequences are
354
+ rejected with this guidance: ordinary shell quotes do not convert them into line breaks.
355
+ Literal `@name` text is also preserved. To create a notification-capable Linear mention, pass the
356
+ repeatable `--mention <name|email|me|id>` option on a comment-writing command.
338
357
 
339
- **Agent skill.** This repo ships a Claude agent skill at `skills/linear-sdk-cli/` that teaches an
340
- agent to drive the CLI — the JSON envelope, exit codes, discovery, forgiving inputs, and how to
341
- install the CLI itself if it is missing. Three ways to install it:
358
+ **Agent skills.** This repo ships portable agent skills in `skills/`: `linear-sdk-cli` teaches an
359
+ agent to drive the CLI, while `linear-sdk-cli-maintenance` defines the repository's safe upkeep
360
+ and release-handoff contract. Both use `SKILL.md` so one source works with Codex and Claude Code.
361
+ Claude Code receives both skills when the repository plugin is installed:
342
362
 
343
363
  ```bash
344
364
  # Claude Code — as a plugin (the repo is its own marketplace)
345
365
  claude plugin marketplace add <owner>/linear-sdk-cli
346
366
  claude plugin install linear-sdk-cli
347
367
 
348
- # any agent that reads SKILL.md — the package ships skills/, so from a clone or an install:
368
+ # from a clone or install, make a portable SKILL.md available to an agent:
349
369
  ln -s "$PWD/skills/linear-sdk-cli" ~/.claude/skills/linear-sdk-cli
350
370
  ```
351
371
 
352
- The skill is self-contained: an agent that has only the skill and no CLI is told to
372
+ For Codex, install or link the same directory into its skills location; no forked prompt is
373
+ needed. The scheduled Claude Code maintenance routine invokes
374
+ `skills/linear-sdk-cli-maintenance/SKILL.md` directly, keeping its schedule separate from the
375
+ versioned maintenance policy. See [`skills/README.md`](skills/README.md) for the interoperability
376
+ contract.
377
+
378
+ The CLI skill is self-contained: an agent that has only the skill and no CLI is told to
353
379
  `bun add -g linear-sdk-cli`, and one arriving from `schpet/linear-cli` is told not to re-enter a
354
380
  key before `linear auth status`, since existing credentials are found. `linear commands --json`
355
381
  gives it the full command surface at runtime, so the reference docs are a starting point, not a
@@ -365,39 +391,39 @@ If your fingers or your scripts learned the other `linear-cli`, most of its spel
365
391
  unchanged. The left column is theirs, the right is the canonical one this CLI documents and prints
366
392
  in `--help`; both are accepted, and passing both at once is a usage error rather than a silent pick.
367
393
 
368
- | linear-cli | here | where |
369
- | --- | --- | --- |
370
- | `-j, --json` | same | every command (global) |
371
- | `-w, --web` | same | `issue view`, `issue pull-request` |
372
- | `--due-date` | `--due` | `issue create`, `issue update` |
373
- | `--target-date` | `--target` | `project`/`milestone`/`initiative` create & update |
374
- | `--start-date` | `--start` | `project create`, `project update` |
375
- | `--search` | `--query` | `issue list`, `issue mine` |
376
- | `--status` | `--state` | `project list` |
377
- | `--all-states` | (no-op) | `issue list` — it already spans every state |
378
- | `--limit 0` | `--all` | every list; `--all` is the spelling we teach |
379
- | `--assignee self` | `me` / `@me` | anywhere a user is named |
380
- | `--cycle active` | `current` | anywhere a cycle is named |
381
- | `--cycle "<name>"` | number, name, or id | all three resolve |
382
- | `issue query` | `issue list` | same command |
383
- | `auth whoami` | `whoami` | both spellings registered |
384
- | `issue comment add\|list\|update\|delete` | `comment add\|list\|…` | both mounted on one implementation |
394
+ | linear-cli | here | where |
395
+ | ----------------------------------------- | ---------------------- | -------------------------------------------------- |
396
+ | `-j, --json` | same | every command (global) |
397
+ | `-w, --web` | same | `issue view`, `issue pull-request` |
398
+ | `--due-date` | `--due` | `issue create`, `issue update` |
399
+ | `--target-date` | `--target` | `project`/`milestone`/`initiative` create & update |
400
+ | `--start-date` | `--start` | `project create`, `project update` |
401
+ | `--search` | `--query` | `issue list`, `issue mine` |
402
+ | `--status` | `--state` | `project list` |
403
+ | `--all-states` | (no-op) | `issue list` — it already spans every state |
404
+ | `--limit 0` | `--all` | every list; `--all` is the spelling we teach |
405
+ | `--assignee self` | `me` / `@me` | anywhere a user is named |
406
+ | `--cycle active` | `current` | anywhere a cycle is named |
407
+ | `--cycle "<name>"` | number, name, or id | all three resolve |
408
+ | `issue query` | `issue list` | same command |
409
+ | `auth whoami` | `whoami` | both spellings registered |
410
+ | `issue comment add\|list\|update\|delete` | `comment add\|list\|…` | both mounted on one implementation |
385
411
 
386
412
  Their query filters all exist here too, under the same names — `issue list`, `issue mine` and
387
413
  `issue search` share one filter set:
388
414
 
389
- | linear-cli | here | notes |
390
- | --- | --- | --- |
391
- | `-U, --unassigned` | same | `issue list`/`search`; passing it with `--assignee` is a usage error |
392
- | `--team A --team B` | same | repeatable **on the three issue queries only**; elsewhere `--team` is the single default-team global |
393
- | `--state a --state b` | same | repeatable; several states OR together (an issue is in one state), and each value is a state name *or* type |
394
- | `--created-after`, `--updated-after` | same | `YYYY-MM-DD` or ISO 8601, inclusive; a malformed date is rejected locally instead of returning an empty list |
395
- | `--project-label` | same | matches the *project's* label; mutually exclusive with `--project` |
396
- | `--milestone` | same | theirs requires `--project`; here that scoping is optional — without it the milestone is matched by name across projects |
397
- | `--search-comments` | `--search-comments` | `issue search` only — the plain list query has nowhere to put it |
398
- | `issue update --team` | same | a real team move: the issue is renumbered, and Linear remaps its state while dropping the cycle, team-scoped labels and any project the new team is not part of |
399
-
400
- Four differences we deliberately did **not** adopt (see `ALIGNMENT.md` for the reasoning): their
415
+ | linear-cli | here | notes |
416
+ | ------------------------------------ | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
417
+ | `-U, --unassigned` | same | `issue list`/`search`; passing it with `--assignee` is a usage error |
418
+ | `--team A --team B` | same | repeatable **on the three issue queries only**; elsewhere `--team` is the single default-team global |
419
+ | `--state a --state b` | same | repeatable; several states OR together (an issue is in one state), and each value is a state name _or_ type |
420
+ | `--created-after`, `--updated-after` | same | `YYYY-MM-DD` or ISO 8601, inclusive; a malformed date is rejected locally instead of returning an empty list |
421
+ | `--project-label` | same | matches the _project's_ label; mutually exclusive with `--project` |
422
+ | `--milestone` | same | theirs requires `--project`; here that scoping is optional — without it the milestone is matched by name across projects |
423
+ | `--search-comments` | `--search-comments` | `issue search` only — the plain list query has nowhere to put it |
424
+ | `issue update --team` | same | a real team move: the issue is renumbered, and Linear remaps its state while dropping the cycle, team-scoped labels and any project the new team is not part of |
425
+
426
+ Four differences we deliberately did **not** adopt: their
401
427
  `issue list` is an alias of `mine` (a `list` that silently filters to you and hides started work is
402
428
  the worst transition hazard, so we added `issue mine` instead of changing `list`); their JSON shape
403
429
  wraps results in connection envelopes and `mine` has no `--json` at all (our uniform bare
@@ -414,7 +440,7 @@ env, the user config, or the OS keyring.
414
440
  Precedence, highest first — `linear config` prints each value with the tier and file it came from:
415
441
 
416
442
  1. the flag (`--team`, `--workspace`, `--sort`)
417
- 2. the environment (`LINEAR_TEAM`/`LINEAR_TEAM_ID`, `LINEAR_WORKSPACE`, `LINEAR_ISSUE_SORT`, `LINEAR_VCS`)
443
+ 2. the environment (`LINEAR_TEAM`/`LINEAR_TEAM_ID`, `LINEAR_WORKSPACE`, `LINEAR_ISSUE_SORT`)
418
444
  3. the **project config**: the first file found walking up from the working directory, checking in
419
445
  each directory `linear.toml`, then `.linear.toml`, then `.config/linear.toml` — the same
420
446
  names and order as [schpet/linear-cli](https://github.com/schpet/linear-cli), so a repo set
@@ -423,7 +449,7 @@ Precedence, highest first — `linear config` prints each value with the tier an
423
449
  5. schpet/linear-cli's **global config**, `~/.config/linear/linear.toml` — read for non-secret
424
450
  settings only, so a migrating user's defaults carry over
425
451
 
426
- Keys: `team` (or `team_id`), `workspace`, `sort` (or `issue_sort`), `vcs`. You can write these from
452
+ Keys: `team` (or `team_id`), `workspace`, and `sort` (or `issue_sort`). You can write these from
427
453
  the CLI — comments and layout in an existing file are preserved, and the write is atomic:
428
454
 
429
455
  ```sh
@@ -442,7 +468,6 @@ is what `auth login` is for.
442
468
  default_workspace = "acme" # which stored credential is active
443
469
  team = "TES" # default team key
444
470
  sort = "priority" # default issue-list sort: priority | updated | created
445
- vcs = "git"
446
471
 
447
472
  [workspaces."acme"] # per-workspace credentials (hyphenated slugs are quoted)
448
473
  keyring = true # secret lives in the OS keyring (service linear-cli, account acme)