linear-sdk-cli 0.1.0

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 (111) hide show
  1. package/.claude-plugin/marketplace.json +20 -0
  2. package/.claude-plugin/plugin.json +9 -0
  3. package/CHANGELOG.md +807 -0
  4. package/COVERAGE.md +495 -0
  5. package/LICENSE +21 -0
  6. package/MIGRATING.md +220 -0
  7. package/README.md +524 -0
  8. package/package.json +87 -0
  9. package/skills/linear-sdk-cli/SKILL.md +534 -0
  10. package/skills/linear-sdk-cli/references/api.md +27 -0
  11. package/skills/linear-sdk-cli/references/attachment.md +82 -0
  12. package/skills/linear-sdk-cli/references/auth.md +174 -0
  13. package/skills/linear-sdk-cli/references/commands.md +37 -0
  14. package/skills/linear-sdk-cli/references/comment.md +154 -0
  15. package/skills/linear-sdk-cli/references/completion.md +17 -0
  16. package/skills/linear-sdk-cli/references/config.md +99 -0
  17. package/skills/linear-sdk-cli/references/cycle.md +131 -0
  18. package/skills/linear-sdk-cli/references/document.md +180 -0
  19. package/skills/linear-sdk-cli/references/favorite.md +78 -0
  20. package/skills/linear-sdk-cli/references/initiative-update.md +67 -0
  21. package/skills/linear-sdk-cli/references/initiative.md +256 -0
  22. package/skills/linear-sdk-cli/references/issue.md +928 -0
  23. package/skills/linear-sdk-cli/references/label.md +112 -0
  24. package/skills/linear-sdk-cli/references/milestone.md +139 -0
  25. package/skills/linear-sdk-cli/references/notification.md +123 -0
  26. package/skills/linear-sdk-cli/references/open.md +29 -0
  27. package/skills/linear-sdk-cli/references/organization.md +99 -0
  28. package/skills/linear-sdk-cli/references/project-update.md +67 -0
  29. package/skills/linear-sdk-cli/references/project.md +234 -0
  30. package/skills/linear-sdk-cli/references/roadmap.md +150 -0
  31. package/skills/linear-sdk-cli/references/schema.md +21 -0
  32. package/skills/linear-sdk-cli/references/state.md +61 -0
  33. package/skills/linear-sdk-cli/references/team.md +227 -0
  34. package/skills/linear-sdk-cli/references/user.md +103 -0
  35. package/skills/linear-sdk-cli/references/webhook.md +151 -0
  36. package/skills/linear-sdk-cli/references/whoami.md +26 -0
  37. package/skills/linear-sdk-cli/scripts/generate-docs.ts +339 -0
  38. package/src/bin/linear.ts +97 -0
  39. package/src/cli.ts +232 -0
  40. package/src/client.ts +172 -0
  41. package/src/commands/agent-session.ts +121 -0
  42. package/src/commands/api.ts +317 -0
  43. package/src/commands/attach.ts +90 -0
  44. package/src/commands/attachment.ts +74 -0
  45. package/src/commands/comment.ts +225 -0
  46. package/src/commands/completion.ts +69 -0
  47. package/src/commands/cycle.ts +125 -0
  48. package/src/commands/discover.ts +170 -0
  49. package/src/commands/document.ts +211 -0
  50. package/src/commands/favorite.ts +72 -0
  51. package/src/commands/initiative-update.ts +47 -0
  52. package/src/commands/initiative.ts +261 -0
  53. package/src/commands/issue.ts +1263 -0
  54. package/src/commands/label.ts +119 -0
  55. package/src/commands/meta.ts +534 -0
  56. package/src/commands/milestone.ts +163 -0
  57. package/src/commands/notification.ts +110 -0
  58. package/src/commands/open.ts +84 -0
  59. package/src/commands/organization.ts +79 -0
  60. package/src/commands/project-update.ts +47 -0
  61. package/src/commands/project.ts +339 -0
  62. package/src/commands/roadmap.ts +131 -0
  63. package/src/commands/state.ts +58 -0
  64. package/src/commands/team.ts +243 -0
  65. package/src/commands/user.ts +107 -0
  66. package/src/commands/webhook.ts +156 -0
  67. package/src/config.ts +1236 -0
  68. package/src/context.ts +117 -0
  69. package/src/git.ts +174 -0
  70. package/src/index.ts +10 -0
  71. package/src/lib/action.ts +26 -0
  72. package/src/lib/body.ts +172 -0
  73. package/src/lib/errors.ts +195 -0
  74. package/src/lib/introspect.ts +150 -0
  75. package/src/lib/keyring.ts +270 -0
  76. package/src/lib/mutation.ts +53 -0
  77. package/src/lib/open.ts +32 -0
  78. package/src/lib/options.ts +640 -0
  79. package/src/lib/output-shapes.ts +562 -0
  80. package/src/lib/pagination.ts +145 -0
  81. package/src/lib/prompt.ts +91 -0
  82. package/src/lib/resolve.ts +596 -0
  83. package/src/lib/shape.ts +273 -0
  84. package/src/lib/status-update.ts +91 -0
  85. package/src/lib/upload.ts +329 -0
  86. package/src/output/color.ts +52 -0
  87. package/src/output/format.ts +238 -0
  88. package/src/output/markdown.ts +133 -0
  89. package/src/output/pager.ts +131 -0
  90. package/src/output/sanitize.ts +76 -0
  91. package/src/output/table.ts +198 -0
  92. package/src/services/agent-session.ts +289 -0
  93. package/src/services/attachment.ts +198 -0
  94. package/src/services/comment.ts +262 -0
  95. package/src/services/cycle.ts +224 -0
  96. package/src/services/document.ts +357 -0
  97. package/src/services/favorite.ts +147 -0
  98. package/src/services/initiative-update.ts +59 -0
  99. package/src/services/initiative.ts +449 -0
  100. package/src/services/issue.ts +1153 -0
  101. package/src/services/label.ts +193 -0
  102. package/src/services/milestone.ts +262 -0
  103. package/src/services/notification.ts +190 -0
  104. package/src/services/organization.ts +144 -0
  105. package/src/services/project-update.ts +36 -0
  106. package/src/services/project.ts +494 -0
  107. package/src/services/roadmap.ts +177 -0
  108. package/src/services/state.ts +127 -0
  109. package/src/services/team.ts +386 -0
  110. package/src/services/user.ts +137 -0
  111. package/src/services/webhook.ts +203 -0
package/README.md ADDED
@@ -0,0 +1,524 @@
1
+ # linear-sdk-cli
2
+
3
+ **An ergonomic command-line interface for [Linear](https://linear.app), built on the official
4
+ [`@linear/sdk`](https://www.npmjs.com/package/@linear/sdk).**
5
+
6
+ It's designed to be pleasant for humans _and_ dependable for scripts and agents. By default you
7
+ get clean, aligned tables and detail views; add `--json` to any command for a stable,
8
+ machine-readable shape. It's git-aware (the "current issue" comes from your branch name) and
9
+ forgiving about input (`--assignee me`, team key `TES`, state and label by name). Anything the
10
+ curated commands don't wrap is still reachable through a raw GraphQL escape hatch (`linear api`),
11
+ so nothing in the Linear API is out of bounds.
12
+
13
+ ```sh
14
+ linear issue list --assignee me --state started # what's on my plate
15
+ linear # the issue for the branch you're on
16
+ linear issue list --json | jq -r '.[].identifier' # ready for scripts
17
+ ```
18
+
19
+ > Design influenced by [`schpet/linear-cli`](https://github.com/schpet/linear-cli) (human-first,
20
+ > git-aware) and [`linearis`](https://github.com/linearis-oss/linearis) (JSON-first for agents).
21
+
22
+ ## Highlights
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.
26
+ - **Agent- and script-friendly** — every data command takes `--json` and emits a stable, documented envelope on stdout; status text stays on stderr.
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.
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.
29
+ - **Forgiving inputs** — refer to things the way you think of them: `TES-123`, team `TES`, `--assignee me`, `--cycle current`, state and label by name.
30
+ - **Multi-workspace** — store credentials for several workspaces and switch with a global `--workspace`.
31
+ - **Complete & honest** — first-class commands for the core resource graph, a raw `linear api` for everything else, and a [measured coverage audit](#coverage) that CI keeps honest.
32
+
33
+ ## Install
34
+
35
+ Requires [Bun](https://bun.sh) **1.1 or newer** and a Linear API key
36
+ (Settings → Security & access → **Personal API keys**). The CLI ships as TypeScript and runs
37
+ directly on Bun — no build step, no bundle, no Node.
38
+
39
+ ```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
44
+ ```
45
+
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
+ This installs two equivalent binaries: **`linear`** and the shorter **`lin`**. If you already
50
+ have a different tool named `linear` on your `PATH`, just use `lin`.
51
+
52
+ ### If you already have `schpet/linear-cli` installed
53
+
54
+ Both CLIs install a binary called **`linear`**, so whichever is earlier on your `PATH` wins and
55
+ the other is silently shadowed — `linear --version` tells you which you got (`0.x` is this one,
56
+ `2.x` is schpet's), and `which -a linear` lists both. Three ways out, pick one:
57
+
58
+ - **Use `lin`.** It is ours alone; nothing of theirs claims it. Every example in this README works
59
+ with `lin` in place of `linear`. Zero-risk for the transition week.
60
+ - **Keep theirs reachable as `linear-schpet`** and let ours have `linear`:
61
+
62
+ ```sh
63
+ # installed with deno: reinstall under a new name, then drop the old one
64
+ deno install -A --reload -f -g -n linear-schpet jsr:@schpet/linear-cli
65
+ deno uninstall -g linear
66
+ # installed with homebrew (schpet/tap/linear): keep the keg, relink under a new name
67
+ brew unlink schpet/tap/linear
68
+ ln -s "$(brew --prefix)/opt/linear/bin/linear" ~/.local/bin/linear-schpet # any dir on your PATH
69
+ # installed with npm/bun globally: rename the shim in place
70
+ mv "$(command -v linear)" "$(dirname "$(command -v linear)")/linear-schpet"
71
+ ```
72
+
73
+ (A package-manager upgrade of theirs may put `linear` back; re-run the line for your channel.)
74
+
75
+ - **Uninstall theirs** once you no longer need it. Your credentials survive: they are in the OS
76
+ keyring under the same service and account, and we read them — see [Authentication](#authentication).
77
+
78
+ Their project-pinned install (`bun add -D @schpet/linear-cli`, run as `bunx linear`) does not
79
+ collide with a global install of ours; inside such a project `bunx linear` is theirs and
80
+ `linear`/`lin` on your `PATH` is ours.
81
+
82
+ <details>
83
+ <summary>Run from source instead</summary>
84
+
85
+ ```sh
86
+ git clone https://github.com/zhendalf/linear-sdk-cli.git && cd linear-sdk-cli
87
+ bun install
88
+ bun run src/bin/linear.ts --help # or: bun run dev -- --help
89
+ ```
90
+
91
+ </details>
92
+
93
+ ## Quickstart
94
+
95
+ ```sh
96
+ export LINEAR_API_KEY=lin_api_xxxxxxxx # quickest way to get going
97
+ linear whoami # confirm you're connected
98
+
99
+ linear issue list --assignee me --state started # my in-progress work
100
+ linear issue view TES-42 # full detail
101
+ linear issue create --title "Fix login" --team TES -P 2 # new High-priority issue
102
+ linear issue start TES-42 # check out its branch + mark it started
103
+ linear issue comment TES-42 "shipped — please review"
104
+ ```
105
+
106
+ In a git repository, bare `linear` (and `linear issue`) shows the issue inferred from the current
107
+ branch, so most issue commands let you omit the id entirely.
108
+
109
+ ## Authentication
110
+
111
+ The CLI resolves your API key in this order: the `--api-key` flag → the `LINEAR_API_KEY`
112
+ environment variable → a plaintext key in the user config file (`~/.config/linear/config.toml`,
113
+ written `0600`) → the **OS keyring** (macOS Keychain, or `secret-tool` on Linux).
114
+
115
+ > **Credential trust boundary.** The API key is **never** read from a project-local
116
+ > `.linear.toml` — only non-secret settings live there — so a key can't be committed by accident.
117
+ > A project may select one of your already-stored credentials with `workspace = "<slug>"`, but it
118
+ > cannot provide or override the secret.
119
+
120
+ ```sh
121
+ linear auth login # store a key (prompts, validates, saves to the keyring)
122
+ linear auth login --plaintext # …or keep it in the 0600 config file instead
123
+ linear auth status # where the key came from (value redacted)
124
+ linear auth migrate # move plaintext keys from the file into the keyring
125
+ linear auth token # print the resolved key (for scripting)
126
+ ```
127
+
128
+ `auth login` stores the secret in the system keyring by default and writes only a
129
+ `keyring = true` marker to the config file; where there is no keyring (other platforms, or a Linux
130
+ box without `libsecret`) it falls back to the file, and says so. The keyring entry is
131
+ `service = linear-cli`, `account = <workspace slug>` — the same convention as
132
+ [schpet/linear-cli](https://github.com/schpet/linear-cli), so if you are coming from it your key is
133
+ found without a re-login (`auth status` reports `Source: keychain`). Note the entry is shared:
134
+ `auth logout` here removes it for both tools. `--key -` reads the key from stdin for scripts;
135
+ passing it as `--key <value>` works but earns a warning, since argv is visible to other processes.
136
+
137
+ ### Multiple workspaces
138
+
139
+ Credentials are stored per **workspace slug**. `auth login` validates the key and derives the slug
140
+ from the key's organization unless you pass `--workspace`:
141
+
142
+ ```sh
143
+ linear auth login --workspace acme # store a key for the "acme" workspace
144
+ linear auth login --workspace other-org # …and another
145
+ linear auth list # show configured workspaces + which is default
146
+ linear auth default acme # choose the default workspace
147
+ linear --workspace other-org issue list # use a specific workspace for one command
148
+ linear auth logout --workspace acme # remove one credential
149
+ ```
150
+
151
+ **Selection precedence** (strict): the `--api-key` flag and `LINEAR_API_KEY` env are absolute and
152
+ bypass selection entirely; otherwise the workspace is chosen by `--workspace` → `LINEAR_WORKSPACE`
153
+ env → project config `workspace` → `default_workspace` in the user config. With one configured
154
+ workspace it's used automatically; with several and no selection, the CLI asks you to pick (via
155
+ `--workspace`, project config, or `auth default`).
156
+
157
+ ## Core concepts
158
+
159
+ A few ideas run through every command:
160
+
161
+ - **Git-branch awareness.** On a `tes-123-*` branch, bare `linear` shows `TES-123` (identical to
162
+ `issue view TES-123`), and nearly every issue subcommand infers the id from the branch — so
163
+ `linear issue comment "…"`, `linear issue start`, `linear issue pr` all "just work" in context.
164
+ - **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
166
+ envelope](#scripting--agents)); status and progress always go to stderr.
167
+ - **Forgiving inputs.** Resolve things by how you think about them: `--assignee me`, assignee by
168
+ email or name, team key `TES`, `--cycle current`, workflow state and label by name (case-
169
+ insensitively). Ambiguous names produce a clear error (exit `3`), not a wrong guess.
170
+
171
+ ## Common workflows
172
+
173
+ **Issue lifecycle**
174
+
175
+ ```sh
176
+ linear issue create --title "Fix login redirect" --team TES -P 2 --assignee me
177
+ linear issue list --assignee me --state started
178
+ linear issue update TES-42 --state "In Review" --add-label backend
179
+ linear issue comment TES-42 "ready for another look"
180
+ linear issue archive TES-42 --yes
181
+ ```
182
+
183
+ **Git + GitHub PR** — turn an issue into commits and a pull request. The id is inferred from the
184
+ branch everywhere below.
185
+
186
+ ```sh
187
+ linear issue start TES-123 # checkout tes-123-* branch and mark it started (--no-move: branch only)
188
+ git commit -m "$(linear issue describe)" # "TES-123 Title" + Linear-issue trailers
189
+ linear issue pr # open a GitHub PR titled "TES-123 Title", body = the trailers
190
+ linear issue pr --draft --base main # …as a draft against a specific base
191
+ linear issue pr --json | jq -r .url # the created PR URL is the only thing on stdout
192
+ ```
193
+
194
+ `issue describe` prints a whole commit message — `TES-123 Title`, a blank line, then two git
195
+ trailers, `Linear-issue: Fixes TES-123` and `Linear-issue-url: <url>` (the same text
196
+ schpet/linear-cli prints, so `git interpret-trailers` and jj read it back). The magic word sits
197
+ right before the id, which is where Linear's
198
+ [git integration](https://linear.app/docs/github#link-prs-and-commits) reads it, so the issue is
199
+ linked — and closed on merge — when the commit lands; `-r` swaps in `References` to link without
200
+ closing. `issue pull-request` (alias `pr`) opens the PR via the [`gh`](https://cli.github.com) CLI:
201
+ title `TES-123 Title` (a custom `--title` is prefixed the same way), body the same two trailers,
202
+ so the PR and issue reference each other; the issue description stays in Linear. It never pushes
203
+ or creates branches for you, and fails with a clear error (not a stack trace) when `gh` is
204
+ missing, unauthenticated, or the branch isn't pushed.
205
+
206
+ **Projects & status updates**
207
+
208
+ ```sh
209
+ linear project list --team TES
210
+ linear project view "Q3 Launch"
211
+ linear project-update create "Q3 Launch" --health onTrack --body "Beta is out to 10% of users."
212
+ linear initiative-update create "Platform" --health atRisk --body-file update.md
213
+ ```
214
+
215
+ Status updates (`project-update`/`pu`, `initiative-update`/`iu`) take the body from `--body`,
216
+ `--body-file <path>` (`-` for stdin), or `--editor` (`$EDITOR`), plus an optional
217
+ `--health <onTrack|atRisk|offTrack>`.
218
+
219
+ ## Command overview
220
+
221
+ 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
223
+ `linear commands --json`.
224
+
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` |
247
+
248
+ <sup>†</sup> Linear has **deprecated roadmaps** in favor of initiatives — reads still work, but the
249
+ API rejects roadmap mutations with a deprecation notice. Use `initiative` for new work.
250
+
251
+ ### Global flags
252
+
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. |
264
+
265
+ ## Scripting & agents
266
+
267
+ The CLI is built to be driven by scripts and agents. Everything in this section is a stable
268
+ contract.
269
+
270
+ **The `--json` envelope.** With `--json`, stdout carries _only_ machine JSON, pretty-printed, one
271
+ value per command:
272
+
273
+ - **list** commands emit a **bare array** (`[...]`) — even when empty (`[]`) and even for a single result.
274
+ - **single-resource** commands (`view`, `whoami`, …) emit a **bare object** (`{...}`).
275
+ - **mutations** (`create`/`update`/`delete`/`archive`/…) emit the affected object — typically a
276
+ small shape like `{ "id", "identifier", "url" }`, or `{ "id", "success": true }` when the API
277
+ returns no body. Destructive commands add a flag such as `{ "deleted": true }` / `{ "archived": true }`.
278
+ - **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
280
+ after it, so `--json --debug` output stays parseable.
281
+
282
+ Status, progress, and pagination notes always go to **stderr**, so `cmd --json` is safe to pipe
283
+ into `jq` unconditionally:
284
+
285
+ ```sh
286
+ linear issue list --json | jq -r '.[].identifier'
287
+ linear issue view TES-42 --json | jq -r '.url'
288
+ ID=$(linear issue create --title "Fix" --team TES --json | jq -r '.id')
289
+ ```
290
+
291
+ **Exit codes** are stable and distinct, so a script can branch on the failure class:
292
+
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 |
302
+
303
+ The error `code` field in the JSON envelope is one of: `usage`, `auth`, `not_found`, `ambiguous`,
304
+ `forbidden`, `validation`, `rate_limited`, `network`, `feature_not_accessible`, `api`, `runtime`.
305
+ Several map to the same exit code (e.g. `ambiguous` → `3`, `validation` → `2`, `forbidden` → `4`),
306
+ so prefer the `code` field for fine-grained handling and the exit code for coarse branching.
307
+
308
+ **Non-interactive flags** make runs deterministic in CI and agent loops:
309
+
310
+ - **`--no-input`** — never prompt; anything that would be prompted for becomes a usage error
311
+ (exit `2`) instead of hanging. Use this whenever there's no human at the keyboard. **`--json`
312
+ implies this**, as does a stdout that is not a TTY: a prompt inside a pipeline is a hang, not a
313
+ question, so you never have to remember both flags.
314
+ - **`-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
316
+ prompt, the command exits **`6`** and reports `{"cancelled": true, "action": "…"}` under `--json`
317
+ — never `0`, so `linear issue delete X && …` cannot run the `&&` side after a "no".
318
+ - **`-q, --quiet`** — suppress success/status lines on stderr (errors still print).
319
+ - **`-n, --limit <n>` / `--all`** — `--limit` caps results; `--all` exhausts pagination. With
320
+ neither, the default cap is **50**. `--all` (and very large `--limit`) can be slow and
321
+ rate-limit-prone on big workspaces.
322
+
323
+ ```sh
324
+ # agent-safe: no prompts, no chatter, fail fast with a parseable error
325
+ linear issue delete TES-42 --yes --no-input --quiet --json
326
+ ```
327
+
328
+ **Discovery — learn the surface without scraping `--help`:**
329
+
330
+ - **`linear commands --json`** — a bare array of `{ path, description, aliases, arguments, options }`
331
+ for every (sub)command, so an agent can enumerate what's available and how to call it.
332
+ - **`linear schema`** — the Linear GraphQL schema as SDL (`-o, --output <file>` writes to a file;
333
+ `--json` prints raw introspection). Pair it with `linear api` to reach anything the curated
334
+ commands don't wrap.
335
+
336
+ ```sh
337
+ linear commands --json | jq -r '.[].path' # every command path
338
+ linear schema -o /tmp/linear.graphql # dump SDL, then explore
339
+ grep 'type Issue ' /tmp/linear.graphql
340
+ ```
341
+
342
+ **File-based bodies & stdin.** Long text (issue descriptions, comments, status updates) can come
343
+ from a file or stdin instead of a flag — e.g. `--body-file <path>` with `-` for stdin, or
344
+ `--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.
347
+
348
+ **Agent skill.** This repo ships a Claude agent skill at `skills/linear-sdk-cli/` that teaches an
349
+ agent to drive the CLI — the JSON envelope, exit codes, discovery, forgiving inputs, and how to
350
+ install the CLI itself if it is missing. Three ways to install it:
351
+
352
+ ```bash
353
+ # Claude Code — as a plugin (the repo is its own marketplace)
354
+ claude plugin marketplace add <owner>/linear-sdk-cli
355
+ claude plugin install linear-sdk-cli
356
+
357
+ # any agent that reads SKILL.md — the package ships skills/, so from a clone or an install:
358
+ ln -s "$PWD/skills/linear-sdk-cli" ~/.claude/skills/linear-sdk-cli
359
+ ```
360
+
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.
366
+
367
+ ## Coming from linear-cli
368
+
369
+ > The full guide — install without a collision, credentials found without a re-login, config
370
+ > read from the same files, and the three places the two CLIs would silently differ — is
371
+ > [`MIGRATING.md`](MIGRATING.md). This section is the flag-level cheat sheet.
372
+
373
+ If your fingers or your scripts learned the other `linear-cli`, most of its spellings work here
374
+ unchanged. The left column is theirs, the right is the canonical one this CLI documents and prints
375
+ in `--help`; both are accepted, and passing both at once is a usage error rather than a silent pick.
376
+
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 |
394
+
395
+ Their query filters all exist here too, under the same names — `issue list`, `issue mine` and
396
+ `issue search` share one filter set:
397
+
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 |
408
+
409
+ Four differences we deliberately did **not** adopt (see `ALIGNMENT.md` for the reasoning): their
410
+ `issue list` is an alias of `mine` (a `list` that silently filters to you and hides started work is
411
+ the worst transition hazard, so we added `issue mine` instead of changing `list`); their JSON shape
412
+ wraps results in connection envelopes and `mine` has no `--json` at all (our uniform bare
413
+ array/object is the point); their short flags are reassigned per command (`-t` is both `--title`
414
+ and `--team` in their own tree, so there is no coherent target to match); and their per-command
415
+ option model, where we keep true globals.
416
+
417
+ ## Configuration
418
+
419
+ Non-secret defaults live in `~/.config/linear/config.toml` (user-wide) or a project-local config
420
+ file. **Secrets never go in a project file** — the API key is only ever read from the flag, the
421
+ env, the user config, or the OS keyring.
422
+
423
+ Precedence, highest first — `linear config` prints each value with the tier and file it came from:
424
+
425
+ 1. the flag (`--team`, `--workspace`, `--sort`)
426
+ 2. the environment (`LINEAR_TEAM`/`LINEAR_TEAM_ID`, `LINEAR_WORKSPACE`, `LINEAR_ISSUE_SORT`)
427
+ 3. the **project config**: the first file found walking up from the working directory, checking in
428
+ each directory `linear.toml`, then `.linear.toml`, then `.config/linear.toml` — the same
429
+ names and order as [schpet/linear-cli](https://github.com/schpet/linear-cli), so a repo set
430
+ up with its `linear config` (which writes `<git root>/.config/linear.toml`) is picked up as-is
431
+ 4. the **user config**, `~/.config/linear/config.toml` (`$XDG_CONFIG_HOME` honored)
432
+ 5. schpet/linear-cli's **global config**, `~/.config/linear/linear.toml` — read for non-secret
433
+ settings only, so a migrating user's defaults carry over
434
+
435
+ Keys: `team` (or `team_id`), `workspace`, and `sort` (or `issue_sort`). You can write these from
436
+ the CLI — comments and layout in an existing file are preserved, and the write is atomic:
437
+
438
+ ```sh
439
+ linear config init # pick a team from a list → <git root>/.linear.toml
440
+ linear config init --team TES # …or say which (scripts); --sort, --path, --force
441
+ linear config set sort updated # edit one key in the project config in effect
442
+ linear config set team ENG --user # …or in ~/.config/linear/config.toml
443
+ linear config # show the result, with each value's source
444
+ ```
445
+
446
+ `config set` will not write `api_key` or anything else that belongs to the credential store — that
447
+ is what `auth login` is for.
448
+
449
+ ```toml
450
+ # ~/.config/linear/config.toml
451
+ default_workspace = "acme" # which stored credential is active
452
+ team = "TES" # default team key
453
+ sort = "priority" # default issue-list sort: priority | updated | created
454
+
455
+ [workspaces."acme"] # per-workspace credentials (hyphenated slugs are quoted)
456
+ keyring = true # secret lives in the OS keyring (service linear-cli, account acme)
457
+ [workspaces."other-org"]
458
+ api_key = "lin_api_yyyyyyyy" # …or, with `auth login --plaintext`, in this 0600 file
459
+ ```
460
+
461
+ Relevant environment variables: **`LINEAR_API_KEY`** (absolute — bypasses workspace selection) and
462
+ **`LINEAR_WORKSPACE`** (selects a stored credential when no flag is given).
463
+
464
+ ### Shell completion
465
+
466
+ ```sh
467
+ source <(linear completion bash) # bash — add to ~/.bashrc
468
+ source <(linear completion zsh) # zsh — add to ~/.zshrc
469
+ linear completion fish > ~/.config/fish/completions/linear.fish
470
+ ```
471
+
472
+ ## Raw API escape hatch
473
+
474
+ Anything without a tailored command is reachable through raw GraphQL — queries or mutations, from
475
+ an argument, a file, or stdin, with variables and optional auto-pagination:
476
+
477
+ ```sh
478
+ linear api '{ viewer { id name } }'
479
+ linear api 'query($id:String!){ issue(id:$id){ title } }' --var id=TES-1
480
+ echo '{ teams { nodes { key name } } }' | linear api --query-file -
481
+ linear api --query-file q.graphql --vars-file vars.json --paginate
482
+ ```
483
+
484
+ Use `linear schema` to discover types and fields first, then reach for `linear api`.
485
+
486
+ ### Coverage
487
+
488
+ Coverage of the SDK is **measured, not asserted.** `linear api` reaches the full GraphQL API, and
489
+ a generated audit ([COVERAGE.md](./COVERAGE.md)) classifies every one of the ~460 `LinearClient`
490
+ members as `curated` (a first-class command), `raw-only` (reachable via `linear api`), or
491
+ `excluded` (admin/integration/SDK plumbing). CI fails on any drift from the committed snapshot, so
492
+ the claim stays honest as the SDK evolves.
493
+
494
+ ## Programmatic use
495
+
496
+ The CLI can also be embedded:
497
+
498
+ ```ts
499
+ import { createProgram } from "linear-sdk-cli";
500
+
501
+ await createProgram().parseAsync(["node", "linear", "issue", "list", "--json"]);
502
+ ```
503
+
504
+ ## Development
505
+
506
+ ```sh
507
+ bun run verify # typecheck + lint + unit/contract tests
508
+ bun run test:live # live integration tests (needs LINEAR_API_KEY + LINEAR_CLI_LIVE=1)
509
+ bun run test:live:admin # also runs admin-tier suites (e.g. team create/update)
510
+ bun run audit:coverage # regenerate COVERAGE.md (add --update to re-baseline the snapshot)
511
+ bun run janitor # sweep leaked `clitest-` fixtures from the test workspace
512
+ ```
513
+
514
+ Architecture is three layers — **commands** (commander wiring) → **services** (one module per
515
+ resource, the only place that touches the SDK) → **`@linear/sdk`**. The machine JSON envelope is
516
+ locked and contract-tested so output never silently drifts.
517
+
518
+ > Live integration tests run against a real workspace and share one API key; running the entire
519
+ > suite repeatedly can hit Linear's rate limit. Run a subset (a few files at a time) or re-run
520
+ > after a short pause if you see transient rate-limit failures.
521
+
522
+ ## License
523
+
524
+ [MIT](./LICENSE) © Eugene Beloded
package/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "linear-sdk-cli",
3
+ "version": "0.1.0",
4
+ "description": "Ergonomic command-line interface for Linear, built on @linear/sdk",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Eugene Beloded <belodedevgenij@gmail.com>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/zhendalf/linear-sdk-cli.git"
11
+ },
12
+ "homepage": "https://github.com/zhendalf/linear-sdk-cli#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/zhendalf/linear-sdk-cli/issues"
15
+ },
16
+ "keywords": [
17
+ "linear",
18
+ "cli",
19
+ "linear-app",
20
+ "issue-tracker",
21
+ "graphql"
22
+ ],
23
+ "bin": {
24
+ "linear": "./src/bin/linear.ts",
25
+ "lin": "./src/bin/linear.ts"
26
+ },
27
+ "main": "./src/index.ts",
28
+ "types": "./src/index.ts",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./src/index.ts",
32
+ "bun": "./src/index.ts",
33
+ "import": "./src/index.ts"
34
+ },
35
+ "./package.json": "./package.json"
36
+ },
37
+ "sideEffects": false,
38
+ "files": [
39
+ ".claude-plugin",
40
+ "CHANGELOG.md",
41
+ "COVERAGE.md",
42
+ "LICENSE",
43
+ "MIGRATING.md",
44
+ "README.md",
45
+ "skills",
46
+ "src"
47
+ ],
48
+ "engines": {
49
+ "bun": ">=1.1.0"
50
+ },
51
+ "scripts": {
52
+ "prepublishOnly": "bun run verify",
53
+ "dev": "bun run src/bin/linear.ts",
54
+ "lin": "bun run src/bin/linear.ts",
55
+ "typecheck": "tsc --noEmit",
56
+ "lint": "eslint .",
57
+ "format": "prettier --write .",
58
+ "format:check": "prettier --check .",
59
+ "test": "bun test test/unit test/contract",
60
+ "test:unit": "bun test test/unit",
61
+ "test:contract": "bun test test/contract",
62
+ "test:live": "LINEAR_CLI_LIVE=1 bun test --timeout 60000 test/integration",
63
+ "test:live:admin": "LINEAR_CLI_LIVE=1 LINEAR_CLI_LIVE_ADMIN=1 bun test --timeout 60000 test/integration",
64
+ "verify": "bun run typecheck && bun run lint && bun run audit:coverage && bun run format:check && bun run test",
65
+ "skill:docs": "bun run skills/linear-sdk-cli/scripts/generate-docs.ts",
66
+ "audit:coverage": "bun run scripts/coverage-audit.ts",
67
+ "janitor": "bun run test/janitor.ts"
68
+ },
69
+ "dependencies": {
70
+ "@inquirer/prompts": "^8.5.2",
71
+ "@linear/sdk": "^89.0.0",
72
+ "commander": "^15.0.0",
73
+ "graphql": "^17.0.2",
74
+ "picocolors": "^1.1.1",
75
+ "smol-toml": "^1.8.0"
76
+ },
77
+ "packageManager": "bun@1.3.14",
78
+ "devDependencies": {
79
+ "@eslint/js": "^10.0.1",
80
+ "@types/bun": "^1.3.14",
81
+ "@types/node": "^26.2.0",
82
+ "eslint": "^10.8.1",
83
+ "prettier": "^3.9.6",
84
+ "typescript": "^6.0.3",
85
+ "typescript-eslint": "^8.67.0"
86
+ }
87
+ }