linear-sdk-cli 0.1.0 → 0.1.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 (71) hide show
  1. package/CHANGELOG.md +46 -46
  2. package/COVERAGE.md +5 -5
  3. package/MIGRATING.md +69 -71
  4. package/README.md +88 -96
  5. package/package.json +3 -4
  6. package/skills/linear-sdk-cli/SKILL.md +3 -13
  7. package/skills/linear-sdk-cli/references/api.md +9 -9
  8. package/skills/linear-sdk-cli/references/auth.md +0 -18
  9. package/skills/linear-sdk-cli/references/commands.md +2 -2
  10. package/skills/linear-sdk-cli/references/config.md +11 -9
  11. package/skills/linear-sdk-cli/references/issue.md +12 -63
  12. package/skills/linear-sdk-cli/references/team.md +0 -16
  13. package/skills/linear-sdk-cli/scripts/generate-docs.ts +2 -6
  14. package/src/bin/linear.ts +6 -16
  15. package/src/cli.ts +1 -3
  16. package/src/commands/agent-session.ts +5 -13
  17. package/src/commands/api.ts +9 -15
  18. package/src/commands/attach.ts +4 -15
  19. package/src/commands/comment.ts +2 -6
  20. package/src/commands/completion.ts +6 -2
  21. package/src/commands/cycle.ts +1 -6
  22. package/src/commands/discover.ts +2 -6
  23. package/src/commands/document.ts +1 -4
  24. package/src/commands/initiative.ts +1 -5
  25. package/src/commands/issue.ts +88 -376
  26. package/src/commands/meta.ts +31 -139
  27. package/src/commands/milestone.ts +1 -4
  28. package/src/commands/notification.ts +6 -15
  29. package/src/commands/project.ts +4 -17
  30. package/src/commands/team.ts +0 -13
  31. package/src/commands/webhook.ts +2 -12
  32. package/src/config.ts +183 -500
  33. package/src/context.ts +4 -13
  34. package/src/git.ts +3 -28
  35. package/src/lib/body.ts +2 -26
  36. package/src/lib/errors.ts +7 -32
  37. package/src/lib/introspect.ts +7 -76
  38. package/src/lib/keyring.ts +9 -51
  39. package/src/lib/options.ts +12 -108
  40. package/src/lib/output-shapes.ts +9 -81
  41. package/src/lib/pagination.ts +17 -50
  42. package/src/lib/resolve.ts +7 -40
  43. package/src/lib/shape.ts +12 -16
  44. package/src/lib/upload.ts +1 -3
  45. package/src/output/format.ts +18 -96
  46. package/src/output/table.ts +7 -7
  47. package/src/services/agent-session.ts +3 -11
  48. package/src/services/attachment.ts +5 -14
  49. package/src/services/comment.ts +4 -12
  50. package/src/services/cycle.ts +4 -11
  51. package/src/services/document.ts +10 -29
  52. package/src/services/favorite.ts +9 -15
  53. package/src/services/initiative-update.ts +15 -16
  54. package/src/services/initiative.ts +10 -15
  55. package/src/services/issue.ts +18 -262
  56. package/src/services/label.ts +2 -12
  57. package/src/services/milestone.ts +5 -8
  58. package/src/services/notification.ts +2 -16
  59. package/src/services/organization.ts +18 -23
  60. package/src/services/project.ts +34 -55
  61. package/src/services/roadmap.ts +6 -7
  62. package/src/services/state.ts +3 -9
  63. package/src/services/team.ts +24 -67
  64. package/src/services/user.ts +2 -2
  65. package/src/services/webhook.ts +17 -28
  66. package/skills/linear-sdk-cli/references/open.md +0 -29
  67. package/src/commands/open.ts +0 -84
  68. package/src/lib/open.ts +0 -32
  69. package/src/output/color.ts +0 -52
  70. package/src/output/markdown.ts +0 -133
  71. package/src/output/pager.ts +0 -131
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
- # Until the first npm release: clone and run from source (see README).
30
+ bun add -g linear-sdk-cli # or: bun link, from a clone
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`; **not** for
63
+ `~/.config/linear/linear.toml` — read for `team_id`, `workspace`, `issue_sort`, `vcs`; **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,91 +70,90 @@ 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. The `vcs` setting
74
- is ignored; this CLI supports Git and does not claim partial jj support. Schpet's
73
+ and is a usage error naming the file — change it to `priority` or drop the line; and schpet's
75
74
  `issue_create_*`, `download_images`, `hyperlink_format`, `attachment_dir` keys are ignored (no
76
75
  such features yet). `linear config init` writes a `.linear.toml`; `linear config set <key>
77
76
  <value>` edits one key.
78
77
 
79
78
  ## 4. Commands — same words, or an alias
80
79
 
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 |
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 |
103
102
 
104
103
  Everything with no row is spelled identically.
105
104
 
106
105
  ## 5. Flags — theirs are accepted
107
106
 
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.
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.
125
124
 
126
125
  **Short flags are the one place we did not follow.** schpet 2.5's own tree assigns `-a` four
127
126
  meanings (`--app`, `--all`, `--assignee`, `--attach`), `-f` four, `-y` three, `-t` two (`--title`
128
127
  and `--team`). There is no consistent target to copy. Ours holds **one meaning per letter across
129
128
  all 141 commands**, so `-t` is always `--team`, `-p` always `--project`, `-P` always `--priority`,
130
129
  `-f` always `--fields`, `-n` always `--limit`. Every collision fails loudly — `-p 2` says "No
131
- project matching '2'", not "set priority 2" — and the two that _used_ not to are loud now: `-f`
130
+ project matching '2'", not "set priority 2" — and the two that *used* not to are loud now: `-f`
132
131
  and `-n` are refused on any command that prints no table (`project create -f desc.md` says
133
132
  "--fields does not apply … use --description-file <path>"; `project create -n 'My Project'` says
134
133
  "--limit does not apply … the name is --name"), instead of creating the project without either.
135
134
 
136
135
  ## 6. What would silently differ — and how we made it loud (or the same)
137
136
 
138
- These are the spots where the same command line could _succeed on both and mean different things_.
137
+ These are the spots where the same command line could *succeed on both and mean different things*.
139
138
  Every one is now either identical, or says so on stderr, or is a usage error. Know them.
140
139
 
141
- 1. **`issue list`** — theirs shows _your unstarted_ issues; ours shows the _team's_ issues.
140
+ 1. **`issue list`** — theirs shows *your unstarted* issues; ours shows the *team's* issues.
142
141
  We did not alias `list` to `mine`: a command named "list" that hides your colleagues' work and
143
142
  your own in-progress work is the sharpest transition hazard there is. Type `issue mine` for the
144
143
  old behavior.
145
144
  2. **Repeated `--label`** narrows (AND) in both CLIs now — same as schpet.
146
145
  3. **`--sort priority`** groups by workflow state in both — but we sort state **ascending** so active
147
- work is on top. schpet hardcodes descending, which the API answers with Backlog _above_ In
146
+ work is on top. schpet hardcodes descending, which the API answers with Backlog *above* In
148
147
  Progress; a Low-priority backlog item outranks an Urgent in-progress one there. We diverge on
149
148
  purpose.
150
- 4. **`user list --all` / `team members --all`** — theirs: include _inactive_ members. Ours: `--all`
149
+ 4. **`user list --all` / `team members --all`** — theirs: include *inactive* members. Ours: `--all`
151
150
  is the global "fetch every page" on all 141 commands, and deactivated users are
152
151
  `--include-disabled`. We kept the one meaning; the command prints a warning on stderr (even
153
152
  under `--quiet`) that deactivated users are still excluded and names `--include-disabled`, and
154
153
  `--help` says so up front. Not adopted: making `--all` mean "and deactivated" on two commands
155
154
  would give one global two meanings.
156
155
  5. **`-f <file>` on `project create/update`, `document create/update`** — theirs: `--description-file` /
157
- `--content-file`. Ours: `-f` is `--fields`, and it used to be _dropped_ on those commands, so
156
+ `--content-file`. Ours: `-f` is `--fields`, and it used to be *dropped* on those commands, so
158
157
  `project create --name X -f desc.md` created the project with no description and exited 0.
159
158
  `--fields`, `--limit` and `--all` are now usage errors on every command that prints no table or
160
159
  detail block, and the message names `--description-file` / `--content-file` where they exist.
@@ -163,7 +162,7 @@ Every one is now either identical, or says so on stderr, or is a usage error. Kn
163
162
  6. **`project create --team A --team B`** — theirs collects both; ours took the last one and created
164
163
  the project in B alone. `--team` on `project create` is now the same repeatable list as
165
164
  `--teams`; both spellings at once is a usage error. (`project update --team` stays refused,
166
- pointing at `--teams`, because that list _replaces_ the project's teams.)
165
+ pointing at `--teams`, because that list *replaces* the project's teams.)
167
166
  7. **`issue start`** — theirs always moves the issue to the first `started` state after branching;
168
167
  ours only did with `--move`, so the transplanted command left the issue in Backlog without a
169
168
  word. Adopted: moving is the default (that is what "start" means, and what an agent that says
@@ -172,37 +171,36 @@ Every one is now either identical, or says so on stderr, or is a usage error. Kn
172
171
  8. **`issue describe`** — theirs prints `ID Title`, a blank line, then `Linear-issue: Fixes ID` and
173
172
  `Linear-issue-url: URL`; ours printed `Title` and a bare `Fixes ID`. Piped into `git commit -m`
174
173
  that was a different commit. Adopted, byte for byte: proper trailers are what `git
175
- interpret-trailers` and jj read back, and Linear reads the magic word right before the id
174
+ interpret-trailers` and jj read back, and Linear reads the magic word right before the id
176
175
  either way (linear.app/docs/github). `-r/--references` still swaps in `References`.
177
176
  9. **`issue pull-request`** — theirs titles the PR `ID Title` and sends the issue URL as the body;
178
- ours titled it `Title` and pasted the issue _description_ into the body. Title adopted (a custom
177
+ ours titled it `Title` and pasted the issue *description* into the body. Title adopted (a custom
179
178
  `--title` is prefixed too). Body: the same two trailers `describe` prints — the URL as there,
180
179
  plus the magic word so the link (and auto-close) does not depend on the branch name — and the
181
180
  description stays in Linear rather than in a GitHub PR body.
182
181
  10. **`issue list`/`mine`/`search`, `project list` with no team configured** — theirs error
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).
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).
187
186
  12. **`document list --team X`** — filters to the team's documents on both.
188
187
 
189
188
  Loud already, listed so you are not surprised:
190
189
 
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.
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.
206
204
  Each is tracked; the raw `linear api` reaches all of the API surface in the meantime. If you type
207
205
  one of these out of habit, the CLI says where the equivalent lives rather than just rejecting it:
208
206
  `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,8 +21,7 @@ linear issue list --json | jq -r '.[].identifier' # ready for scripts
21
21
 
22
22
  ## Highlights
23
23
 
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.
24
+ - **Human-first by default** — aligned tables and readable detail views, color-aware, paged sanely.
26
25
  - **Agent- and script-friendly** — every data command takes `--json` and emits a stable, documented envelope on stdout; status text stays on stderr.
27
26
  - **Git-aware** — the current issue is inferred from your branch (`tes-123-fix` → `TES-123`), so most issue commands let you drop the id.
28
27
  - **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.
@@ -37,15 +36,10 @@ Requires [Bun](https://bun.sh) **1.1 or newer** and a Linear API key
37
36
  directly on Bun — no build step, no bundle, no Node.
38
37
 
39
38
  ```sh
40
- git clone https://github.com/zhendalf/linear-sdk-cli.git
41
- cd linear-sdk-cli
42
- bun install
43
- bun run dev -- --help
39
+ bun add -g linear-sdk-cli # or, for a one-off: bunx linear-sdk-cli --help
40
+ linear --help
44
41
  ```
45
42
 
46
- The npm package name has not been published yet. Source installation is the supported route until
47
- the maintainers choose package ownership and publish the first release.
48
-
49
43
  This installs two equivalent binaries: **`linear`** and the shorter **`lin`**. If you already
50
44
  have a different tool named `linear` on your `PATH`, just use `lin`.
51
45
 
@@ -71,7 +65,6 @@ the other is silently shadowed — `linear --version` tells you which you got (`
71
65
  ```
72
66
 
73
67
  (A package-manager upgrade of theirs may put `linear` back; re-run the line for your channel.)
74
-
75
68
  - **Uninstall theirs** once you no longer need it. Your credentials survive: they are in the OS
76
69
  keyring under the same service and account, and we read them — see [Authentication](#authentication).
77
70
 
@@ -83,11 +76,10 @@ collide with a global install of ours; inside such a project `bunx linear` is th
83
76
  <summary>Run from source instead</summary>
84
77
 
85
78
  ```sh
86
- git clone https://github.com/zhendalf/linear-sdk-cli.git && cd linear-sdk-cli
79
+ git clone <this-repo> && cd linear-sdk-cli
87
80
  bun install
88
81
  bun run src/bin/linear.ts --help # or: bun run dev -- --help
89
82
  ```
90
-
91
83
  </details>
92
84
 
93
85
  ## Quickstart
@@ -162,7 +154,7 @@ A few ideas run through every command:
162
154
  `issue view TES-123`), and nearly every issue subcommand infers the id from the branch — so
163
155
  `linear issue comment "…"`, `linear issue start`, `linear issue pr` all "just work" in context.
164
156
  - **Human by default, `--json` for machines.** Without `--json` you get tables and detail views
165
- meant to be read. With `--json`, stdout carries _only_ machine JSON (a [stable
157
+ meant to be read. With `--json`, stdout carries *only* machine JSON (a [stable
166
158
  envelope](#scripting--agents)); status and progress always go to stderr.
167
159
  - **Forgiving inputs.** Resolve things by how you think about them: `--assignee me`, assignee by
168
160
  email or name, team key `TES`, `--cycle current`, workflow state and label by name (case-
@@ -219,55 +211,55 @@ Status updates (`project-update`/`pu`, `initiative-update`/`iu`) take the body f
219
211
  ## Command overview
220
212
 
221
213
  Every group has `--help` with full options and (for the busy ones) an Examples section. Aliases
222
- are shown in parentheses. For a machine-readable tree of _every_ command, run
214
+ are shown in parentheses. For a machine-readable tree of *every* command, run
223
215
  `linear commands --json`.
224
216
 
225
- | Group | What you can do |
226
- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
227
- | **`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` |
228
- | **`team`** (`t`) | `list` · `view` · `members` · `states` · `labels` · `cycles` · `create` · `update` |
229
- | **`project`** (`p`) | `list` · `view` · `create` · `update` · `archive` · `milestones` |
230
- | **`project-update`** (`pu`) | `create` · `list` (project status updates, with `--health`) |
231
- | **`milestone`** (`m`) | `list` · `view` · `create` · `update` · `delete` |
232
- | **`cycle`** (`c`) | `list` · `view` · `current` · `create` · `update` |
233
- | **`user`** (`u`) | `list` · `view` · `me` |
234
- | **`label`** (`lb`) | `list` · `create` · `update` · `delete` |
235
- | **`state`** (`st`) | `list` · `view` (workflow states) |
236
- | **`comment`** (`cm`) | `list` · `add` · `reply` · `update` · `delete` · `resolve`/`unresolve` |
237
- | **`document`** (`doc`) | `list` · `view` · `create` · `update` · `delete` |
238
- | **`attachment`** (`at`) | `list` · `create` · `delete` |
239
- | **`favorite`** (`fav`) | `list` · `add` · `remove` |
240
- | **`initiative`** (`init`) | `list` · `view` · `create` · `update` · `archive` · `delete` |
241
- | **`initiative-update`** (`iu`) | `create` · `list` (initiative status updates, with `--health`) |
242
- | **`roadmap`** (`rm`) | `list` · `view` · `create` · `update` · `delete` &nbsp;<sup>†</sup> |
243
- | **`notification`** (`notif`) | `list` · `read`/`unread` · `read-all` · `archive` · `snooze` |
244
- | **`organization`** (`org`) | `view` · `members` · `invites` |
245
- | **`webhook`** (`wh`) | `list` · `view` · `create` · `update` · `delete` |
246
- | **top-level** | `whoami` · `auth` (`login` · `list` · `default` · `token` · `status` · `logout`) · `config` · `api` · `commands` · `schema` · `completion` |
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` |
247
239
 
248
240
  <sup>†</sup> Linear has **deprecated roadmaps** in favor of initiatives — reads still work, but the
249
241
  API rejects roadmap mutations with a deprecation notice. Use `initiative` for new work.
250
242
 
251
243
  ### Global flags
252
244
 
253
- | Flag | Effect |
254
- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
255
- | `--json` | Emit machine JSON only on stdout (see [the contract](#scripting--agents)). |
256
- | `-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. |
257
- | `-n, --limit <n>` / `--all` | Cap results, or fetch every page. Refused on a command that pages nothing. |
258
- | `-t, --team <key>` | Set the default team for the command. |
259
- | `--workspace <slug>` | Select which stored workspace credential to use. |
260
- | `-y, --yes` | Skip confirmation prompts (required for destructive actions when not a TTY). |
261
- | `--no-input` | Never prompt; fail with a usage error instead of hanging. |
262
- | `--no-ansi` | Disable colored output (`--no-color` is accepted as an alias). |
263
- | `-q, --quiet` · `--debug` | Silence status output · verbose errors. |
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. |
264
256
 
265
257
  ## Scripting & agents
266
258
 
267
259
  The CLI is built to be driven by scripts and agents. Everything in this section is a stable
268
260
  contract.
269
261
 
270
- **The `--json` envelope.** With `--json`, stdout carries _only_ machine JSON, pretty-printed, one
262
+ **The `--json` envelope.** With `--json`, stdout carries *only* machine JSON, pretty-printed, one
271
263
  value per command:
272
264
 
273
265
  - **list** commands emit a **bare array** (`[...]`) — even when empty (`[]`) and even for a single result.
@@ -276,7 +268,7 @@ value per command:
276
268
  small shape like `{ "id", "identifier", "url" }`, or `{ "id", "success": true }` when the API
277
269
  returns no body. Destructive commands add a flag such as `{ "deleted": true }` / `{ "archived": true }`.
278
270
  - **errors** go to **stderr** as `{"error":{"message":"…","code":"…"}}` and never to stdout. With
279
- `--debug`, the extra detail is carried _inside_ that object as `error.detail` rather than appended
271
+ `--debug`, the extra detail is carried *inside* that object as `error.detail` rather than appended
280
272
  after it, so `--json --debug` output stays parseable.
281
273
 
282
274
  Status, progress, and pagination notes always go to **stderr**, so `cmd --json` is safe to pipe
@@ -290,15 +282,15 @@ ID=$(linear issue create --title "Fix" --team TES --json | jq -r '.id')
290
282
 
291
283
  **Exit codes** are stable and distinct, so a script can branch on the failure class:
292
284
 
293
- | Code | Name | When |
294
- | ---: | ------------------- | ------------------------------------------------------------------- |
295
- | `0` | ok | success |
296
- | `1` | runtime/API | network/GraphQL/other runtime failure (also feature-not-accessible) |
297
- | `2` | usage | bad flags/arguments, missing required input, validation |
298
- | `3` | not-found/ambiguous | the referenced resource doesn't exist, or a name matched many |
299
- | `4` | auth | missing/invalid API key, or forbidden |
300
- | `5` | rate-limited | Linear rate limit hit |
301
- | `6` | cancelled | a confirmation prompt was declined — nothing was changed |
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 |
302
294
 
303
295
  The error `code` field in the JSON envelope is one of: `usage`, `auth`, `not_found`, `ambiguous`,
304
296
  `forbidden`, `validation`, `rate_limited`, `network`, `feature_not_accessible`, `api`, `runtime`.
@@ -312,7 +304,7 @@ so prefer the `code` field for fine-grained handling and the exit code for coars
312
304
  implies this**, as does a stdout that is not a TTY: a prompt inside a pipeline is a hang, not a
313
305
  question, so you never have to remember both flags.
314
306
  - **`-y, --yes`** — pre-confirm destructive actions (`delete`/`archive`). Without a TTY,
315
- destructive commands _require_ `--yes` (they refuse rather than block). If a human declines the
307
+ destructive commands *require* `--yes` (they refuse rather than block). If a human declines the
316
308
  prompt, the command exits **`6`** and reports `{"cancelled": true, "action": "…"}` under `--json`
317
309
  — never `0`, so `linear issue delete X && …` cannot run the `&&` side after a "no".
318
310
  - **`-q, --quiet`** — suppress success/status lines on stderr (errors still print).
@@ -342,8 +334,7 @@ grep 'type Issue ' /tmp/linear.graphql
342
334
  **File-based bodies & stdin.** Long text (issue descriptions, comments, status updates) can come
343
335
  from a file or stdin instead of a flag — e.g. `--body-file <path>` with `-` for stdin, or
344
336
  `--editor` to open `$EDITOR`. The raw `linear api` reads from `--query-file -` and `--vars-file -`
345
- too, so you can pipe GraphQL straight in. Inline bodies containing literal `\n` sequences are
346
- rejected with this guidance: ordinary shell quotes do not convert them into line breaks.
337
+ too, so you can pipe GraphQL straight in.
347
338
 
348
339
  **Agent skill.** This repo ships a Claude agent skill at `skills/linear-sdk-cli/` that teaches an
349
340
  agent to drive the CLI — the JSON envelope, exit codes, discovery, forgiving inputs, and how to
@@ -358,11 +349,11 @@ claude plugin install linear-sdk-cli
358
349
  ln -s "$PWD/skills/linear-sdk-cli" ~/.claude/skills/linear-sdk-cli
359
350
  ```
360
351
 
361
- The skill is self-contained: an agent that has only the skill is told to install from a source
362
- clone until the first npm release, and one arriving from `schpet/linear-cli` is told not to
363
- re-enter a key before `linear auth status`, since existing credentials are found. `linear commands
364
- --json` gives it the full command surface at runtime, so the reference docs are a starting point,
365
- not a cage.
352
+ The skill is self-contained: an agent that has only the skill and no CLI is told to
353
+ `bun add -g linear-sdk-cli`, and one arriving from `schpet/linear-cli` is told not to re-enter a
354
+ key before `linear auth status`, since existing credentials are found. `linear commands --json`
355
+ gives it the full command surface at runtime, so the reference docs are a starting point, not a
356
+ cage.
366
357
 
367
358
  ## Coming from linear-cli
368
359
 
@@ -374,37 +365,37 @@ If your fingers or your scripts learned the other `linear-cli`, most of its spel
374
365
  unchanged. The left column is theirs, the right is the canonical one this CLI documents and prints
375
366
  in `--help`; both are accepted, and passing both at once is a usage error rather than a silent pick.
376
367
 
377
- | linear-cli | here | where |
378
- | ----------------------------------------- | ---------------------- | -------------------------------------------------- |
379
- | `-j, --json` | same | every command (global) |
380
- | `-w, --web` | same | `issue view`, `issue pull-request` |
381
- | `--due-date` | `--due` | `issue create`, `issue update` |
382
- | `--target-date` | `--target` | `project`/`milestone`/`initiative` create & update |
383
- | `--start-date` | `--start` | `project create`, `project update` |
384
- | `--search` | `--query` | `issue list`, `issue mine` |
385
- | `--status` | `--state` | `project list` |
386
- | `--all-states` | (no-op) | `issue list` — it already spans every state |
387
- | `--limit 0` | `--all` | every list; `--all` is the spelling we teach |
388
- | `--assignee self` | `me` / `@me` | anywhere a user is named |
389
- | `--cycle active` | `current` | anywhere a cycle is named |
390
- | `--cycle "<name>"` | number, name, or id | all three resolve |
391
- | `issue query` | `issue list` | same command |
392
- | `auth whoami` | `whoami` | both spellings registered |
393
- | `issue comment add\|list\|update\|delete` | `comment add\|list\|…` | both mounted on one implementation |
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
385
 
395
386
  Their query filters all exist here too, under the same names — `issue list`, `issue mine` and
396
387
  `issue search` share one filter set:
397
388
 
398
- | linear-cli | here | notes |
399
- | ------------------------------------ | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
400
- | `-U, --unassigned` | same | `issue list`/`search`; passing it with `--assignee` is a usage error |
401
- | `--team A --team B` | same | repeatable **on the three issue queries only**; elsewhere `--team` is the single default-team global |
402
- | `--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 |
403
- | `--created-after`, `--updated-after` | same | `YYYY-MM-DD` or ISO 8601, inclusive; a malformed date is rejected locally instead of returning an empty list |
404
- | `--project-label` | same | matches the _project's_ label; mutually exclusive with `--project` |
405
- | `--milestone` | same | theirs requires `--project`; here that scoping is optional — without it the milestone is matched by name across projects |
406
- | `--search-comments` | `--search-comments` | `issue search` only — the plain list query has nowhere to put it |
407
- | `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 |
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 |
408
399
 
409
400
  Four differences we deliberately did **not** adopt (see `ALIGNMENT.md` for the reasoning): their
410
401
  `issue list` is an alias of `mine` (a `list` that silently filters to you and hides started work is
@@ -423,7 +414,7 @@ env, the user config, or the OS keyring.
423
414
  Precedence, highest first — `linear config` prints each value with the tier and file it came from:
424
415
 
425
416
  1. the flag (`--team`, `--workspace`, `--sort`)
426
- 2. the environment (`LINEAR_TEAM`/`LINEAR_TEAM_ID`, `LINEAR_WORKSPACE`, `LINEAR_ISSUE_SORT`)
417
+ 2. the environment (`LINEAR_TEAM`/`LINEAR_TEAM_ID`, `LINEAR_WORKSPACE`, `LINEAR_ISSUE_SORT`, `LINEAR_VCS`)
427
418
  3. the **project config**: the first file found walking up from the working directory, checking in
428
419
  each directory `linear.toml`, then `.linear.toml`, then `.config/linear.toml` — the same
429
420
  names and order as [schpet/linear-cli](https://github.com/schpet/linear-cli), so a repo set
@@ -432,7 +423,7 @@ Precedence, highest first — `linear config` prints each value with the tier an
432
423
  5. schpet/linear-cli's **global config**, `~/.config/linear/linear.toml` — read for non-secret
433
424
  settings only, so a migrating user's defaults carry over
434
425
 
435
- Keys: `team` (or `team_id`), `workspace`, and `sort` (or `issue_sort`). You can write these from
426
+ Keys: `team` (or `team_id`), `workspace`, `sort` (or `issue_sort`), `vcs`. You can write these from
436
427
  the CLI — comments and layout in an existing file are preserved, and the write is atomic:
437
428
 
438
429
  ```sh
@@ -451,6 +442,7 @@ is what `auth login` is for.
451
442
  default_workspace = "acme" # which stored credential is active
452
443
  team = "TES" # default team key
453
444
  sort = "priority" # default issue-list sort: priority | updated | created
445
+ vcs = "git"
454
446
 
455
447
  [workspaces."acme"] # per-workspace credentials (hyphenated slugs are quoted)
456
448
  keyring = true # secret lives in the OS keyring (service linear-cli, account acme)