linear-sdk-cli 0.2.4 → 0.3.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 (42) hide show
  1. package/CHANGELOG.md +36 -16
  2. package/MIGRATING.md +7 -6
  3. package/README.md +120 -31
  4. package/package.json +5 -3
  5. package/skills/linear-sdk-cli/SKILL.md +43 -8
  6. package/skills/linear-sdk-cli/references/api.md +1 -1
  7. package/skills/linear-sdk-cli/references/attachment.md +1 -1
  8. package/skills/linear-sdk-cli/references/auth.md +26 -7
  9. package/skills/linear-sdk-cli/references/commands.md +1 -1
  10. package/skills/linear-sdk-cli/references/comment.md +1 -1
  11. package/skills/linear-sdk-cli/references/completion.md +1 -1
  12. package/skills/linear-sdk-cli/references/config.md +5 -1
  13. package/skills/linear-sdk-cli/references/cycle.md +1 -1
  14. package/skills/linear-sdk-cli/references/document.md +1 -1
  15. package/skills/linear-sdk-cli/references/favorite.md +1 -1
  16. package/skills/linear-sdk-cli/references/initiative-update.md +1 -1
  17. package/skills/linear-sdk-cli/references/initiative.md +1 -1
  18. package/skills/linear-sdk-cli/references/issue.md +1 -1
  19. package/skills/linear-sdk-cli/references/label.md +1 -1
  20. package/skills/linear-sdk-cli/references/milestone.md +1 -1
  21. package/skills/linear-sdk-cli/references/notification.md +1 -1
  22. package/skills/linear-sdk-cli/references/open.md +1 -1
  23. package/skills/linear-sdk-cli/references/organization.md +1 -1
  24. package/skills/linear-sdk-cli/references/project-update.md +1 -1
  25. package/skills/linear-sdk-cli/references/project.md +1 -1
  26. package/skills/linear-sdk-cli/references/roadmap.md +1 -1
  27. package/skills/linear-sdk-cli/references/schema.md +1 -1
  28. package/skills/linear-sdk-cli/references/state.md +1 -1
  29. package/skills/linear-sdk-cli/references/team.md +1 -1
  30. package/skills/linear-sdk-cli/references/user.md +1 -1
  31. package/skills/linear-sdk-cli/references/webhook.md +1 -1
  32. package/skills/linear-sdk-cli/references/whoami.md +1 -1
  33. package/skills/linear-sdk-cli/scripts/generate-docs.ts +3 -1
  34. package/src/client.ts +57 -2
  35. package/src/commands/meta.ts +240 -65
  36. package/src/config.ts +319 -16
  37. package/src/context.ts +2 -0
  38. package/src/index.ts +19 -0
  39. package/src/lib/introspect.ts +1 -0
  40. package/src/lib/options.ts +1 -0
  41. package/src/lib/output-shapes.ts +21 -2
  42. package/src/oauth.ts +582 -0
package/CHANGELOG.md CHANGED
@@ -1,8 +1,21 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to this project are documented here. The format is based on
4
- [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
5
- [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
3
+ Release Please generates this file from Conventional Commits. Feature and fix pull requests do
4
+ not edit it directly; their squash-commit titles are the release-note source of truth. This project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.3.1](https://github.com/zhendalf/linear-sdk-cli/compare/v0.3.0...v0.3.1) (2026-08-28)
8
+
9
+ ### Bug Fixes
10
+
11
+ - **release:** prevent changelog drift ([e8019a0](https://github.com/zhendalf/linear-sdk-cli/commit/e8019a0cc59182579237f9ec83138259fcd0615b))
12
+
13
+ ## [0.3.0](https://github.com/zhendalf/linear-sdk-cli/compare/v0.2.4...v0.3.0) (2026-08-28)
14
+
15
+ ### Features
16
+
17
+ - **auth:** add browser OAuth login with PKCE (LIN-740) ([d5826e1](https://github.com/zhendalf/linear-sdk-cli/commit/d5826e1ebdc9015e876faa29b004f4953a190b43))
18
+ - **auth:** support OAuth access tokens ([#28](https://github.com/zhendalf/linear-sdk-cli/issues/28)) ([4fecb6c](https://github.com/zhendalf/linear-sdk-cli/commit/4fecb6c1a40533804ff2fde5ace689d39c70d4bc))
6
19
 
7
20
  ## [0.2.4](https://github.com/zhendalf/linear-sdk-cli/compare/v0.2.3...v0.2.4) (2026-08-24)
8
21
 
@@ -33,27 +46,39 @@ All notable changes to this project are documented here. The format is based on
33
46
  ### Features
34
47
 
35
48
  - expand CLI workflows and terminal UX ([0fe31cc](https://github.com/zhendalf/linear-sdk-cli/commit/0fe31cca7e71d0fa6eee2b7c1461ecaf0a01103f))
49
+ - **comments:** add explicit, notification-capable mentions with repeatable `--mention`
50
+ ([TES-738](https://linear.app/issue/TES-738)). Literal `@name` text remains Markdown; comment
51
+ commands resolve users exactly and prepend real Linear mention tokens.
36
52
 
37
53
  ### Bug Fixes
38
54
 
39
55
  - **ci:** give release dispatch repository context ([#15](https://github.com/zhendalf/linear-sdk-cli/issues/15)) ([e3f51e6](https://github.com/zhendalf/linear-sdk-cli/commit/e3f51e600102416f3bd3212578b364a13a452e49))
40
56
 
57
+ ### Changed
58
+
59
+ - Replace ESLint with Oxlint while preserving the existing JavaScript and TypeScript checks.
60
+ - Use TypeScript 7 for type-checking without source or configuration compatibility changes.
61
+
41
62
  ## [0.1.3](https://github.com/zhendalf/linear-sdk-cli/compare/v0.1.2...v0.1.3) (2026-08-24)
42
63
 
43
64
  ### Bug Fixes
44
65
 
45
66
  - **ci:** grant OIDC to automated publisher ([#7](https://github.com/zhendalf/linear-sdk-cli/issues/7)) ([acc6e4b](https://github.com/zhendalf/linear-sdk-cli/commit/acc6e4b9c4ab485ce39eeec7eb2c835892016e59))
46
67
 
47
- ## [Unreleased]
68
+ ## [0.1.2](https://github.com/zhendalf/linear-sdk-cli/compare/v0.1.1...v0.1.2) (2026-08-24)
69
+
70
+ ### Dependencies
71
+
72
+ - Upgrade the runtime and development dependencies ([#2](https://github.com/zhendalf/linear-sdk-cli/issues/2)).
73
+
74
+ ### Continuous Integration
75
+
76
+ - Run verification on pull requests using the latest Bun release ([#3](https://github.com/zhendalf/linear-sdk-cli/issues/3)).
77
+
78
+ ## [0.1.1](https://github.com/zhendalf/linear-sdk-cli/releases/tag/v0.1.1) (2026-08-24)
48
79
 
49
80
  ### Added
50
81
 
51
- - **Explicit, notification-capable comment mentions (`--mention`, TES-738).** Ordinary `@name`
52
- text remains literal Markdown. Comment-writing commands now accept repeatable
53
- `--mention <name|email|me|id>`, resolve each user exactly, deduplicate repeats, and prepend real
54
- Linear mention tokens as their own paragraph. The option is available on `issue comment`,
55
- `comment add`, `comment reply`, and `comment update`, including the shared
56
- `issue comment add/update` mounts.
57
82
  - **Every command declares its `--json` output, and `linear commands <path>` prints it (TES-610).**
58
83
  An agent driving the CLI cold guessed key names and filed false bugs (`.user` read as `.author`,
59
84
  `.parent` as `.parentId`); the field names were knowable only from the source. Each node of
@@ -326,11 +351,6 @@ true` is sent unless `--no-default-template` (also accepted under the reference'
326
351
 
327
352
  ### Changed
328
353
 
329
- - **Development linting now uses Oxlint instead of ESLint.** The migrated configuration preserves
330
- the existing recommended JavaScript and TypeScript checks and underscore-prefixed unused-variable
331
- exceptions while reducing the lint dependency tree and execution time.
332
- - **TypeScript 7 now powers type-checking.** The native compiler replaces TypeScript 6 without
333
- source or configuration compatibility changes in this project.
334
354
  - **`--fields`, `--limit` and `--all` are refused on commands that never read them** (TES-637 (2),
335
355
  TES-596). All twelve globals are registered on every command so they can sit anywhere on the
336
356
  line, but `--fields` projects a _rendered_ result (a table or a detail block) and `--limit`/`--all`
@@ -835,7 +855,7 @@ column 36`), never an excerpt from a credential-bearing file, and strip control
835
855
  - **`roadmap delete` alias is now `rm`** (was `del`), matching the `rm` delete alias used by
836
856
  every other group.
837
857
 
838
- ## [0.1.0]
858
+ ## [0.1.0] (2026-06-26)
839
859
 
840
860
  Initial release. An ergonomic CLI for Linear built on `@linear/sdk`, with human-readable output
841
861
  by default and a stable `--json` mode for scripts and agents.
package/MIGRATING.md CHANGED
@@ -47,12 +47,13 @@ linear auth list # …lists it, Storage: keychain
47
47
  ```
48
48
 
49
49
  If it does not — no keyring on this platform, or you used schpet's plaintext mode (`slug =
50
- "lin_api_…"` inline in `credentials.toml`, which we deliberately do not read) — `linear auth login`
51
- prompts (masked) and stores the key in the keyring; `--plaintext` keeps it in our `0600`
52
- `config.toml` instead. `auth migrate` moves plaintext credentials from that file into the keyring,
53
- same command name as theirs. Because the keyring entry is shared, `auth logout` here removes it for
54
- schpet too (and drops the slug from its list file, so neither tool reports a workspace whose key is
55
- gone). The API key is **never** read from a project `.linear.toml`.
50
+ "lin_api_…"` inline in `credentials.toml`, which we deliberately do not read) — pipe the key to
51
+ `linear auth login --key -`; `--plaintext` keeps it in our `0600` `config.toml` instead. Bare
52
+ `linear auth login` now starts browser OAuth, so it never prompts for a personal API key. `auth
53
+ migrate` moves plaintext credentials from our file into the keyring, same command name as theirs.
54
+ Because the API-key entry is shared, `auth logout` here removes it for both tools (and drops the
55
+ slug from schpet's list file, so neither reports a workspace whose key is gone). The API key is
56
+ **never** read from a project `.linear.toml`.
56
57
 
57
58
  ## 3. Config — same files, same keys
58
59
 
package/README.md CHANGED
@@ -103,51 +103,133 @@ branch, so most issue commands let you omit the id entirely.
103
103
 
104
104
  ## Authentication
105
105
 
106
- The CLI resolves your API key in this order: the `--api-key` flag → the `LINEAR_API_KEY`
107
- environment variable → a plaintext key in the user config file (`~/.config/linear/config.toml`,
108
- written `0600`) → the **OS keyring** (macOS Keychain, or `secret-tool` on Linux).
109
-
110
- > **Credential trust boundary.** The API key is **never** read from a project-local
106
+ For a person at a terminal, `linear auth login` uses browser Authorization Code + PKCE and the
107
+ default `actor=user`. Existing personal API keys and invocation-scoped OAuth access tokens remain
108
+ supported. Explicit `--api-key` or `--access-token` flags override environment credentials;
109
+ without a credential flag the CLI reads `LINEAR_API_KEY` or `LINEAR_ACCESS_TOKEN`. If both kinds
110
+ are supplied at the same precedence level, the CLI fails rather than silently choosing a Linear
111
+ actor.
112
+
113
+ > **Credential trust boundary.** A secret is **never** read from a project-local
111
114
  > `.linear.toml` — only non-secret settings live there — so a key can't be committed by accident.
112
115
  > A project may select one of your already-stored credentials with `workspace = "<slug>"`, but it
113
- > cannot provide or override the secret.
116
+ > cannot provide or override the secret. Browser OAuth credentials are keyring-only; an access
117
+ > token injected with `--access-token` or `LINEAR_ACCESS_TOKEN` remains invocation-scoped.
118
+
119
+ ```sh
120
+ linear auth login # browser OAuth, read + write, actor=user
121
+ linear auth login --read-only # request only Linear's read scope
122
+ linear auth login --no-browser # print the URL; still waits on the loopback callback
123
+ printf '%s\n' "$LINEAR_API_KEY" | linear auth login --key - # personal API-key fallback
124
+ linear auth status # credential kind and source (value redacted)
125
+ linear auth logout --workspace acme # revoke OAuth (if used) and remove the profile
126
+ ```
127
+
128
+ Browser login generates a cryptographically random verifier and CSRF state, requires PKCE S256,
129
+ opens Linear, and listens temporarily on the registered `127.0.0.1` callback. It validates the
130
+ viewer and workspace before saving the access token, rotating refresh token, expiry, granted
131
+ scopes, OAuth client identity, and workspace identity. The whole record lives only in the system
132
+ keyring (`service = linear-cli`, `account = oauth:<workspace slug>`); the config file gets only
133
+ non-secret `keyring = true` and `oauth = true` markers. Access tokens refresh before expiry and
134
+ once after an authentication failure, with refresh-token rotation committed under the credential
135
+ store lock. `--admin` explicitly adds the admin scope; it is never implicit.
136
+
137
+ `--no-browser` is useful over SSH only when the machine running the CLI is also reachable as the
138
+ browser's loopback host; Linear does not currently document a device-code flow. Override a
139
+ separately registered public client's identity with `LINEAR_OAUTH_CLIENT_ID` and its exact callback
140
+ with `LINEAR_OAUTH_REDIRECT_URI`. No OAuth client secret is embedded, accepted, or stored.
141
+
142
+ For a personal API key, `auth login --key -` reads stdin, validates the viewer/workspace, stores the
143
+ key in the system keyring by default, and writes only a marker to the config. `--plaintext` is an
144
+ API-key-only compatibility option. API-key entries use `account = <workspace slug>`, matching
145
+ [schpet/linear-cli](https://github.com/schpet/linear-cli). Passing a key as `--key <value>` works but
146
+ warns because argv is visible to other processes. `auth migrate` moves legacy plaintext keys into
147
+ the keyring, and `auth token` exports stored API keys only; OAuth tokens are never printed.
148
+
149
+ ### Apps, agents, and service accounts
150
+
151
+ Use an OAuth access token when the CLI runs inside an integration or agent:
114
152
 
115
153
  ```sh
116
- linear auth login # store a key (prompts, validates, saves to the keyring)
117
- linear auth login --plaintext # …or keep it in the 0600 config file instead
118
- linear auth status # where the key came from (value redacted)
119
- linear auth migrate # move plaintext keys from the file into the keyring
120
- linear auth token # print the resolved key (for scripting)
154
+ LINEAR_ACCESS_TOKEN="$access_token" linear whoami --json
155
+ LINEAR_ACCESS_TOKEN="$access_token" linear issue list --json
121
156
  ```
122
157
 
123
- `auth login` stores the secret in the system keyring by default and writes only a
124
- `keyring = true` marker to the config file; where there is no keyring (other platforms, or a Linux
125
- box without `libsecret`) it falls back to the file, and says so. The keyring entry is
126
- `service = linear-cli`, `account = <workspace slug>` — the same convention as
127
- [schpet/linear-cli](https://github.com/schpet/linear-cli), so if you are coming from it your key is
128
- found without a re-login (`auth status` reports `Source: keychain`). Note the entry is shared:
129
- `auth logout` here removes it for both tools. `--key -` reads the key from stdin for scripts;
130
- passing it as `--key <value>` works but earns a warning, since argv is visible to other processes.
158
+ The application host owns OAuth installation, refresh-token or client-credentials exchange,
159
+ client-secret storage, and webhook processing; it injects only the current access token into each
160
+ CLI invocation. The CLI passes it to `LinearClient({ accessToken })`, which sends the required
161
+ Bearer authorization. Prefer the environment variable because a command-line token is visible to
162
+ other processes and shell history.
163
+
164
+ For a long-lived, single-workspace host, enable client credentials on the Linear OAuth app and keep
165
+ its client ID and secret in the host's secret manager. The package exports an expiry-aware provider
166
+ that caches Linear's 30-day app-actor token in memory, coalesces concurrent exchanges, renews five
167
+ minutes before expiry, and can be forced to renew after a `401`:
168
+
169
+ ```ts
170
+ import { ClientCredentialsTokenProvider } from "linear-sdk-cli";
171
+
172
+ const { LINEAR_CLIENT_ID: clientId, LINEAR_CLIENT_SECRET: clientSecret, ...childEnv } = process.env;
173
+ if (!clientId || !clientSecret) throw new Error("Linear OAuth credentials are missing");
174
+
175
+ const tokens = new ClientCredentialsTokenProvider({
176
+ clientId,
177
+ clientSecret,
178
+ scopes: ["read", "write"],
179
+ });
180
+
181
+ const { accessToken } = await tokens.getAccessToken();
182
+ const child = Bun.spawn(["linear", "issue", "list", "--json"], {
183
+ env: { ...childEnv, LINEAR_ACCESS_TOKEN: accessToken },
184
+ });
185
+ await child.exited;
186
+ ```
187
+
188
+ Call `tokens.invalidate()` (or `getAccessToken({ forceRefresh: true })`) before one bounded retry
189
+ when Linear rejects the token with `401`. Client-credentials tokens do not have refresh tokens;
190
+ renewal is another authenticated exchange. A short-lived/serverless process should use a secure
191
+ shared token cache or central broker rather than minting a new 30-day token on every invocation.
192
+
193
+ For an app installed into multiple workspaces, use Authorization Code with `actor=app` instead and
194
+ store each installation's rotating refresh token encrypted in the host. That hosted app lifecycle
195
+ remains distinct from this CLI's browser PKCE login for a human `actor=user`.
196
+
197
+ For a distinct app identity, install the OAuth application with `actor=app`. Native Linear agents
198
+ can additionally request `app:mentionable` and `app:assignable` and subscribe to Agent Session
199
+ webhooks. App actors cannot request the `admin` scope. See Linear's
200
+ [OAuth guide](https://linear.app/developers/oauth-2-0-authentication),
201
+ [app-actor authorization](https://linear.app/developers/oauth-actor-authorization), and
202
+ [agent guide](https://linear.app/developers/agents).
203
+
204
+ Hosted app tokens supplied through `LINEAR_ACCESS_TOKEN` remain invocation-scoped. The only OAuth
205
+ tokens persisted by the CLI are human browser-login sessions, stored as an isolated per-workspace
206
+ keyring record; the CLI never persists or uses an OAuth client secret.
131
207
 
132
208
  ### Multiple workspaces
133
209
 
134
- Credentials are stored per **workspace slug**. `auth login` validates the key and derives the slug
135
- from the key's organization unless you pass `--workspace`:
210
+ Credentials are stored per **workspace slug**. Browser and API-key login both derive the slug from
211
+ the authenticated organization; `--workspace` validates that the result matches:
136
212
 
137
213
  ```sh
138
- linear auth login --workspace acme # store a key for the "acme" workspace
139
- linear auth login --workspace other-org # …and another
214
+ linear auth login --workspace acme # browser OAuth for "acme"
215
+ linear auth login --workspace other-org # …and another workspace
216
+ linear auth login --workspace acme --key - # personal API-key fallback via stdin
140
217
  linear auth list # show configured workspaces + which is default
141
218
  linear auth default acme # choose the default workspace
142
219
  linear --workspace other-org issue list # use a specific workspace for one command
143
- linear auth logout --workspace acme # remove one credential
220
+ linear auth logout --workspace acme # revoke OAuth, then remove one credential
144
221
  ```
145
222
 
146
- **Selection precedence** (strict): the `--api-key` flag and `LINEAR_API_KEY` env are absolute and
147
- bypass selection entirely; otherwise the workspace is chosen by `--workspace` → `LINEAR_WORKSPACE`
148
- env → project config `workspace` → `default_workspace` in the user config. With one configured
149
- workspace it's used automatically; with several and no selection, the CLI asks you to pick (via
150
- `--workspace`, project config, or `auth default`).
223
+ If revocation is intentionally unavailable, `auth logout --local-only` removes only local state.
224
+ If browser login temporarily superseded an existing personal API-key profile for that workspace,
225
+ OAuth logout preserves and reactivates the API key instead of deleting it.
226
+
227
+ **Selection precedence** (strict): an explicit `--api-key` or `--access-token` flag bypasses the
228
+ environment and workspace selection entirely. Otherwise `LINEAR_API_KEY` or
229
+ `LINEAR_ACCESS_TOKEN` is absolute. With no invocation-scoped credential, the workspace is chosen
230
+ by `--workspace` → `LINEAR_WORKSPACE` env → project config `workspace` → `default_workspace` in the
231
+ user config. With one configured workspace it's used automatically; with several and no selection,
232
+ the CLI asks you to pick (via `--workspace`, project config, or `auth default`).
151
233
 
152
234
  ## Core concepts
153
235
 
@@ -475,8 +557,9 @@ keyring = true # secret lives in the OS keyring (service linear-
475
557
  api_key = "lin_api_yyyyyyyy" # …or, with `auth login --plaintext`, in this 0600 file
476
558
  ```
477
559
 
478
- Relevant environment variables: **`LINEAR_API_KEY`** (absolute — bypasses workspace selection) and
479
- **`LINEAR_WORKSPACE`** (selects a stored credential when no flag is given).
560
+ Relevant environment variables: **`LINEAR_API_KEY`** or **`LINEAR_ACCESS_TOKEN`** (absolute —
561
+ bypasses workspace selection) and **`LINEAR_WORKSPACE`** (selects a stored API-key credential when
562
+ no invocation-scoped credential is given).
480
563
 
481
564
  ### Shell completion
482
565
 
@@ -524,6 +607,7 @@ await createProgram().parseAsync(["node", "linear", "issue", "list", "--json"]);
524
607
  bun run verify # typecheck + lint + unit/contract tests
525
608
  bun run test:live # live integration tests (needs LINEAR_API_KEY + LINEAR_CLI_LIVE=1)
526
609
  bun run test:live:admin # also runs admin-tier suites (e.g. team create/update)
610
+ bun run audit:changelog # verify package, tag, and generated release-note history
527
611
  bun run audit:coverage # regenerate COVERAGE.md (add --update to re-baseline the snapshot)
528
612
  bun run janitor # sweep leaked `clitest-` fixtures from the test workspace
529
613
  ```
@@ -532,6 +616,11 @@ Architecture is three layers — **commands** (commander wiring) → **services*
532
616
  resource, the only place that touches the SDK) → **`@linear/sdk`**. The machine JSON envelope is
533
617
  locked and contract-tested so output never silently drifts.
534
618
 
619
+ Release Please owns `CHANGELOG.md`. Pull requests use Conventional Commit titles and are squash
620
+ merged, giving each change one release-note entry. Do not edit the changelog in a feature or fix
621
+ pull request; use a `BEGIN_COMMIT_OVERRIDE` block in the pull request body when a change needs
622
+ several or specially worded release notes.
623
+
535
624
  > Live integration tests run against a real workspace and share one API key; running the entire
536
625
  > suite repeatedly can hit Linear's rate limit. Run a subset (a few files at a time) or re-run
537
626
  > after a short pause if you see transient rate-limit failures.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linear-sdk-cli",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
4
4
  "description": "Ergonomic command-line interface for Linear, built on @linear/sdk",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -61,8 +61,10 @@
61
61
  "test:contract": "bun test test/contract",
62
62
  "test:live": "LINEAR_CLI_LIVE=1 bun test --timeout 60000 test/integration",
63
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",
64
+ "verify": "bun run typecheck && bun run lint && bun run audit:changelog && bun run audit:coverage && bun run format:check && bun run test",
65
65
  "skill:docs": "bun run skills/linear-sdk-cli/scripts/generate-docs.ts",
66
+ "audit:changelog": "bun run scripts/changelog-audit.ts",
67
+ "audit:pr-title": "bun run scripts/pr-title-audit.ts",
66
68
  "audit:coverage": "bun run scripts/coverage-audit.ts",
67
69
  "janitor": "bun run test/janitor.ts"
68
70
  },
@@ -77,7 +79,7 @@
77
79
  "packageManager": "bun@1.4.0",
78
80
  "devDependencies": {
79
81
  "@types/bun": "^1.4.0",
80
- "@types/node": "^26.2.0",
82
+ "@types/node": "^26.4.0",
81
83
  "oxfmt": "^0.65.0",
82
84
  "oxlint": "^1.80.0",
83
85
  "typescript": "^7.0.2"
@@ -35,23 +35,57 @@ lin --version
35
35
 
36
36
  ### Authentication
37
37
 
38
- The CLI needs a Linear API key. Three ways to provide one, in order of precedence:
38
+ The CLI supports distinct human, hosted-app, and personal-key lifecycles:
39
39
 
40
- 1. **`--api-key <key>`** flag (per invocation) or **`LINEAR_API_KEY`** env var — best
40
+ 1. **Human browser OAuth (default)** via Authorization Code + PKCE S256, `actor=user`:
41
+
42
+ ```bash
43
+ linear auth login # opens Linear; read + write scopes
44
+ linear auth login --read-only # read scope only
45
+ linear auth login --no-browser # prints URL; loopback callback still required
46
+ ```
47
+
48
+ The CLI validates the viewer/workspace and stores the access token, rotating refresh token,
49
+ expiry, scopes, client identity, and workspace identity only in the OS keyring. It refreshes
50
+ before expiry and once after an authentication failure. `--admin` is explicit; no client secret
51
+ is embedded. `auth logout` revokes this grant before removing it (`--local-only` skips
52
+ revocation). Remote/headless callers need the loopback host reachable; Linear has no documented
53
+ device-code flow.
54
+
55
+ 2. **Invocation OAuth access token** via `--access-token <token>` or `LINEAR_ACCESS_TOKEN` — use
56
+ this for hosted Linear apps, agents, and service accounts. Prefer the environment variable so
57
+ the secret does not appear in argv:
58
+
59
+ ```bash
60
+ LINEAR_ACCESS_TOKEN=... linear whoami --json
61
+ ```
62
+
63
+ The host application owns OAuth installation, token refresh/client-credentials exchange,
64
+ client-secret storage, and webhooks. These tokens remain invocation-scoped.
65
+ Long-lived hosts can import `ClientCredentialsTokenProvider` from `linear-sdk-cli`; it caches
66
+ the 30-day app token in memory, renews before expiry, coalesces concurrent exchanges, and
67
+ supports invalidation or forced renewal for one bounded retry after a `401`. Keep the client
68
+ secret in the host's secret manager and inject only `LINEAR_ACCESS_TOKEN` into CLI children.
69
+ Serverless hosts need a secure shared token cache or broker so cold starts do not mint a new
70
+ token for every command.
71
+
72
+ 3. **`--api-key <key>`** flag (per invocation) or **`LINEAR_API_KEY`** env var — best
41
73
  for CI and ephemeral agent runs:
42
74
 
43
75
  ```bash
44
76
  LINEAR_API_KEY=lin_api_... linear whoami --json
45
77
  ```
46
78
 
47
- 2. **Stored credentials** via `linear auth login` (validates the key and saves it):
79
+ 4. **Stored personal API-key credentials** via the explicit compatibility path:
48
80
 
49
81
  ```bash
50
- linear auth login --key lin_api_... # non-interactive
51
- linear auth login # prompts for the key (interactive only)
82
+ printf '%s\n' "$LINEAR_API_KEY" | linear auth login --key -
52
83
  ```
53
84
 
54
- 3. **Multiple workspaces** — store several credentials and select one per call with
85
+ Passing `--key <value>` also works but exposes it in argv and produces a warning. API keys may
86
+ use `--plaintext`; browser OAuth is always keyring-only.
87
+
88
+ 5. **Multiple workspaces** — store several OAuth or API-key credentials and select one per call with
55
89
  `--workspace <slug>`; set a default with `linear auth default <slug>`:
56
90
 
57
91
  ```bash
@@ -60,8 +94,9 @@ The CLI needs a Linear API key. Three ways to provide one, in order of precedenc
60
94
  linear issue list --workspace acme --json
61
95
  ```
62
96
 
63
- Inspect resolution with `linear auth status`. Get the resolved key for scripting
64
- (e.g. raw `curl`) with `linear auth token`.
97
+ An explicit credential flag overrides environment credentials. If both credential kinds occur at
98
+ the same precedence level, the CLI fails rather than silently choosing an actor. Inspect resolution
99
+ with `linear auth status`. `linear auth token` exports stored API keys only and never OAuth tokens.
65
100
 
66
101
  ## Agent best practices
67
102
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  _Generated from `linear commands --json`. `linear api --help` (or `<subcommand> --help`) is authoritative._
6
6
 
7
- Every command also accepts the global flags `-j/--json`, `--no-input`, `-y/--yes`, `-q/--quiet`, `--workspace <slug>`, `--api-key <key>`, `-t/--team <key>`, `-n/--limit <n>`, `--all`, `-f/--fields <a,b,c>`, `--no-ansi` (alias `--no-color`), and `--debug`. Only command-specific options are listed below.
7
+ Every command also accepts the global flags `-j/--json`, `--no-input`, `-y/--yes`, `-q/--quiet`, `--workspace <slug>`, `--api-key <key>`, `--access-token <token>`, `-t/--team <key>`, `-n/--limit <n>`, `--all`, `-f/--fields <a,b,c>`, `--no-ansi` (alias `--no-color`), and `--debug`. Only command-specific options are listed below.
8
8
 
9
9
  ### `linear api`
10
10
 
@@ -6,7 +6,7 @@ Group alias: `at`
6
6
 
7
7
  _Generated from `linear commands --json`. `linear attachment --help` (or `<subcommand> --help`) is authoritative._
8
8
 
9
- Every command also accepts the global flags `-j/--json`, `--no-input`, `-y/--yes`, `-q/--quiet`, `--workspace <slug>`, `--api-key <key>`, `-t/--team <key>`, `-n/--limit <n>`, `--all`, `-f/--fields <a,b,c>`, `--no-ansi` (alias `--no-color`), and `--debug`. Only command-specific options are listed below.
9
+ Every command also accepts the global flags `-j/--json`, `--no-input`, `-y/--yes`, `-q/--quiet`, `--workspace <slug>`, `--api-key <key>`, `--access-token <token>`, `-t/--team <key>`, `-n/--limit <n>`, `--all`, `-f/--fields <a,b,c>`, `--no-ansi` (alias `--no-color`), and `--debug`. Only command-specific options are listed below.
10
10
 
11
11
  ### `linear attachment`
12
12
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  _Generated from `linear commands --json`. `linear auth --help` (or `<subcommand> --help`) is authoritative._
6
6
 
7
- Every command also accepts the global flags `-j/--json`, `--no-input`, `-y/--yes`, `-q/--quiet`, `--workspace <slug>`, `--api-key <key>`, `-t/--team <key>`, `-n/--limit <n>`, `--all`, `-f/--fields <a,b,c>`, `--no-ansi` (alias `--no-color`), and `--debug`. Only command-specific options are listed below.
7
+ Every command also accepts the global flags `-j/--json`, `--no-input`, `-y/--yes`, `-q/--quiet`, `--workspace <slug>`, `--api-key <key>`, `--access-token <token>`, `-t/--team <key>`, `-n/--limit <n>`, `--all`, `-f/--fields <a,b,c>`, `--no-ansi` (alias `--no-color`), and `--debug`. Only command-specific options are listed below.
8
8
 
9
9
  ### `linear auth`
10
10
 
@@ -63,29 +63,39 @@ linear auth list [options]
63
63
  ```text
64
64
  slug: string
65
65
  isDefault: boolean
66
+ credentialType: string
66
67
  storage: string
67
68
  ```
68
69
 
69
70
  ### `linear auth login`
70
71
 
71
- Validate and store a Linear API key for a workspace
72
+ Authenticate in the browser with OAuth, or store a personal API key
72
73
 
73
74
  ```
74
75
  linear auth login [options]
75
76
  ```
76
77
 
77
- | Option | Description |
78
- | ------------- | --------------------------------------------------------------------- |
79
- | `--key <key>` | API key (otherwise prompted; '-' reads it from stdin) |
80
- | `--plaintext` | Store the key in the config file (0600) instead of the system keyring |
78
+ | Option | Description |
79
+ | ---------------------- | --------------------------------------------------------------------- |
80
+ | `--key <key>` | use a personal API key ('-' reads it from stdin) |
81
+ | `--plaintext` | Store the key in the config file (0600) instead of the system keyring |
82
+ | `--no-browser` | print the authorization URL instead of opening it |
83
+ | `--read-only` | request read-only OAuth access |
84
+ | `--admin` | explicitly add the OAuth admin scope |
85
+ | `--timeout <seconds>` | seconds to wait for the loopback callback |
86
+ | `--client-id <id>` | OAuth client ID (defaults to the packaged CLI app) |
87
+ | `--redirect-uri <uri>` | registered HTTP loopback callback URI |
81
88
 
82
89
  **Output (`--json`)**: a receipt object
83
90
 
84
91
  ```text
85
92
  success: boolean
93
+ credentialType: string
86
94
  workspace: string
87
95
  user: {id: string, name: string, email: string}
88
96
  storage: string
97
+ scopes?: string[]
98
+ expiresAt?: string
89
99
  path: string
90
100
  ```
91
101
 
@@ -97,12 +107,18 @@ Remove a stored workspace credential (select with --workspace <slug>)
97
107
  linear auth logout [options]
98
108
  ```
99
109
 
110
+ | Option | Description |
111
+ | -------------- | ------------------------------------------------- |
112
+ | `--local-only` | remove local credentials without OAuth revocation |
113
+
100
114
  **Output (`--json`)**: a receipt object
101
115
 
102
116
  ```text
103
117
  success: boolean
104
118
  workspace: string
105
119
  removed: boolean
120
+ revocation: string
121
+ fallbackCredentialType: string | null
106
122
  ```
107
123
 
108
124
  ### `linear auth migrate`
@@ -123,7 +139,7 @@ path: string
123
139
 
124
140
  ### `linear auth status`
125
141
 
126
- Show where the API key is resolved from (key redacted)
142
+ Show where the active credential is resolved from (value redacted)
127
143
 
128
144
  ```
129
145
  linear auth status [options]
@@ -133,10 +149,13 @@ linear auth status [options]
133
149
 
134
150
  ```text
135
151
  authenticated: boolean
152
+ credentialType: string | null
136
153
  source: string
137
154
  workspace: string | null
138
155
  key: string
139
156
  keyring: string | null
157
+ scopes: string[] | null
158
+ expiresAt: string | null
140
159
  ```
141
160
 
142
161
  ### `linear auth token`
@@ -4,7 +4,7 @@
4
4
 
5
5
  _Generated from `linear commands --json`. `linear commands --help` (or `<subcommand> --help`) is authoritative._
6
6
 
7
- Every command also accepts the global flags `-j/--json`, `--no-input`, `-y/--yes`, `-q/--quiet`, `--workspace <slug>`, `--api-key <key>`, `-t/--team <key>`, `-n/--limit <n>`, `--all`, `-f/--fields <a,b,c>`, `--no-ansi` (alias `--no-color`), and `--debug`. Only command-specific options are listed below.
7
+ Every command also accepts the global flags `-j/--json`, `--no-input`, `-y/--yes`, `-q/--quiet`, `--workspace <slug>`, `--api-key <key>`, `--access-token <token>`, `-t/--team <key>`, `-n/--limit <n>`, `--all`, `-f/--fields <a,b,c>`, `--no-ansi` (alias `--no-color`), and `--debug`. Only command-specific options are listed below.
8
8
 
9
9
  ### `linear commands`
10
10
 
@@ -6,7 +6,7 @@ Group alias: `cm`
6
6
 
7
7
  _Generated from `linear commands --json`. `linear comment --help` (or `<subcommand> --help`) is authoritative._
8
8
 
9
- Every command also accepts the global flags `-j/--json`, `--no-input`, `-y/--yes`, `-q/--quiet`, `--workspace <slug>`, `--api-key <key>`, `-t/--team <key>`, `-n/--limit <n>`, `--all`, `-f/--fields <a,b,c>`, `--no-ansi` (alias `--no-color`), and `--debug`. Only command-specific options are listed below.
9
+ Every command also accepts the global flags `-j/--json`, `--no-input`, `-y/--yes`, `-q/--quiet`, `--workspace <slug>`, `--api-key <key>`, `--access-token <token>`, `-t/--team <key>`, `-n/--limit <n>`, `--all`, `-f/--fields <a,b,c>`, `--no-ansi` (alias `--no-color`), and `--debug`. Only command-specific options are listed below.
10
10
 
11
11
  ### `linear comment`
12
12
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  _Generated from `linear commands --json`. `linear completion --help` (or `<subcommand> --help`) is authoritative._
6
6
 
7
- Every command also accepts the global flags `-j/--json`, `--no-input`, `-y/--yes`, `-q/--quiet`, `--workspace <slug>`, `--api-key <key>`, `-t/--team <key>`, `-n/--limit <n>`, `--all`, `-f/--fields <a,b,c>`, `--no-ansi` (alias `--no-color`), and `--debug`. Only command-specific options are listed below.
7
+ Every command also accepts the global flags `-j/--json`, `--no-input`, `-y/--yes`, `-q/--quiet`, `--workspace <slug>`, `--api-key <key>`, `--access-token <token>`, `-t/--team <key>`, `-n/--limit <n>`, `--all`, `-f/--fields <a,b,c>`, `--no-ansi` (alias `--no-color`), and `--debug`. Only command-specific options are listed below.
8
8
 
9
9
  ### `linear completion`
10
10
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  _Generated from `linear commands --json`. `linear config --help` (or `<subcommand> --help`) is authoritative._
6
6
 
7
- Every command also accepts the global flags `-j/--json`, `--no-input`, `-y/--yes`, `-q/--quiet`, `--workspace <slug>`, `--api-key <key>`, `-t/--team <key>`, `-n/--limit <n>`, `--all`, `-f/--fields <a,b,c>`, `--no-ansi` (alias `--no-color`), and `--debug`. Only command-specific options are listed below.
7
+ Every command also accepts the global flags `-j/--json`, `--no-input`, `-y/--yes`, `-q/--quiet`, `--workspace <slug>`, `--api-key <key>`, `--access-token <token>`, `-t/--team <key>`, `-n/--limit <n>`, `--all`, `-f/--fields <a,b,c>`, `--no-ansi` (alias `--no-color`), and `--debug`. Only command-specific options are listed below.
8
8
 
9
9
  ### `linear config`
10
10
 
@@ -19,6 +19,8 @@ linear config [options]
19
19
  ```text
20
20
  apiKey: string
21
21
  apiKeySource: string
22
+ accessToken: string
23
+ accessTokenSource: string
22
24
  credentialWorkspace: string | null
23
25
  team: string | null
24
26
  workspace: string | null
@@ -88,6 +90,8 @@ linear config show [options]
88
90
  ```text
89
91
  apiKey: string
90
92
  apiKeySource: string
93
+ accessToken: string
94
+ accessTokenSource: string
91
95
  credentialWorkspace: string | null
92
96
  team: string | null
93
97
  workspace: string | null
@@ -6,7 +6,7 @@ Group alias: `c`
6
6
 
7
7
  _Generated from `linear commands --json`. `linear cycle --help` (or `<subcommand> --help`) is authoritative._
8
8
 
9
- Every command also accepts the global flags `-j/--json`, `--no-input`, `-y/--yes`, `-q/--quiet`, `--workspace <slug>`, `--api-key <key>`, `-t/--team <key>`, `-n/--limit <n>`, `--all`, `-f/--fields <a,b,c>`, `--no-ansi` (alias `--no-color`), and `--debug`. Only command-specific options are listed below.
9
+ Every command also accepts the global flags `-j/--json`, `--no-input`, `-y/--yes`, `-q/--quiet`, `--workspace <slug>`, `--api-key <key>`, `--access-token <token>`, `-t/--team <key>`, `-n/--limit <n>`, `--all`, `-f/--fields <a,b,c>`, `--no-ansi` (alias `--no-color`), and `--debug`. Only command-specific options are listed below.
10
10
 
11
11
  ### `linear cycle`
12
12
 
@@ -6,7 +6,7 @@ Group alias: `doc`
6
6
 
7
7
  _Generated from `linear commands --json`. `linear document --help` (or `<subcommand> --help`) is authoritative._
8
8
 
9
- Every command also accepts the global flags `-j/--json`, `--no-input`, `-y/--yes`, `-q/--quiet`, `--workspace <slug>`, `--api-key <key>`, `-t/--team <key>`, `-n/--limit <n>`, `--all`, `-f/--fields <a,b,c>`, `--no-ansi` (alias `--no-color`), and `--debug`. Only command-specific options are listed below.
9
+ Every command also accepts the global flags `-j/--json`, `--no-input`, `-y/--yes`, `-q/--quiet`, `--workspace <slug>`, `--api-key <key>`, `--access-token <token>`, `-t/--team <key>`, `-n/--limit <n>`, `--all`, `-f/--fields <a,b,c>`, `--no-ansi` (alias `--no-color`), and `--debug`. Only command-specific options are listed below.
10
10
 
11
11
  ### `linear document`
12
12
 
@@ -6,7 +6,7 @@ Group alias: `fav`
6
6
 
7
7
  _Generated from `linear commands --json`. `linear favorite --help` (or `<subcommand> --help`) is authoritative._
8
8
 
9
- Every command also accepts the global flags `-j/--json`, `--no-input`, `-y/--yes`, `-q/--quiet`, `--workspace <slug>`, `--api-key <key>`, `-t/--team <key>`, `-n/--limit <n>`, `--all`, `-f/--fields <a,b,c>`, `--no-ansi` (alias `--no-color`), and `--debug`. Only command-specific options are listed below.
9
+ Every command also accepts the global flags `-j/--json`, `--no-input`, `-y/--yes`, `-q/--quiet`, `--workspace <slug>`, `--api-key <key>`, `--access-token <token>`, `-t/--team <key>`, `-n/--limit <n>`, `--all`, `-f/--fields <a,b,c>`, `--no-ansi` (alias `--no-color`), and `--debug`. Only command-specific options are listed below.
10
10
 
11
11
  ### `linear favorite`
12
12