Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
},
"metadata": {
"description": "Marketplace for the linearis Claude Code plugin.",
"version": "1.0.0"
"version": "1.0.1"
},
"plugins": [
{
"name": "linearis",
"source": "./",
"description": "Agent skill teaching agents to use the linearis Linear.app CLI.",
"version": "1.0.0",
"version": "1.0.1",
"author": { "name": "linearis-oss" },
"homepage": "https://github.com/linearis-oss/linearis",
"license": "MIT"
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "linearis",
"description": "Agent skill for the linearis Linear.app CLI: preflight, discover-then-act usage protocol, JSON output, ID resolution, discussions, files.",
"version": "1.0.0",
"version": "1.0.1",
"author": {
"name": "linearis-oss",
"url": "https://github.com/linearis-oss/linearis"
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
/src/gql/
USAGE.md

# Demo recordings: intermediate casts and raw session captures are regenerable
# (see scripts/*-demo-*-cast.mjs); only the rendered SVGs in docs/assets/ are committed.
docs/assets/*.cast
docs/assets/*.jsonl

# clean-publish staging dir (release publish; see .releaserc.cjs)
/.clean-pkg/

Expand Down
91 changes: 91 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ Need a CLI command?
→ Use createContext() → resolve IDs → call service → outputSuccess()
→ Register in src/main.ts (setupXCommands + META in allMetas[])
→ Add DomainMeta export + usage subcommand
→ Add interactive support (see Interactive Prompts): *CreateSpec/*UpdateSpec
for create/update; optional [id] positional + entity picker for id commands

Need tests?
→ Add tests/unit/{resolvers,services,common}/*.test.ts
Expand Down Expand Up @@ -192,6 +194,72 @@ Registration checklist:
3. Add meta to `allMetas[]` in `src/main.ts`.
4. Run `npm run generate:usage` to update `USAGE.md`.

## Interactive Prompts

Human users get an optional wizard; agents and pipes get untouched JSON. The
descriptor-driven engine in `src/common/interactive/` **gathers input only** — it
sits above the Resolver→Service→`outputSuccess` pipeline and never changes it.
Choice `value`s are the human strings a user would type (team key, project name) or
a UUID passthrough, so **resolvers still run** and layer separation holds. All prompt
UI is on **stderr**; stdout stays byte-identical JSON. **Never compromise the
machine/agent contract to add interactivity.**

### When to add support

- **New `create`/`update` command** → declare an `export const *CreateSpec` /
`*UpdateSpec` (`PromptSpec<O>`) colocated at the top of the command file, next to
the options interface it mirrors. Field order encodes cross-field deps.
- **New command with a leading entity-id positional** (`read`/`update`/`delete`/
`archive`/`react`) → make the positional **optional** (`[id]`, not `<id>`) and pass
an entity `picker` so the engine can fill it when absent.
- **Otherwise** (`list`/`search`, a free-text positional whose value is user-typed
rather than chosen from an enumerable source, or a required 2nd positional) →
**skip**, and if it keeps a required leading `<arg>` add its verb to
`SKIP_REQUIRED_POSITIONAL` in the coverage test with a one-line reason.

### Call site (one insertion per action; body below is unchanged)

```typescript
const filled = await maybeCollectInteractive<CreateWizardOptions, never>(
ctx, getRootOpts(command), {
spec: issueCreateSpec,
options: { ...options, ...(title !== undefined ? { title } : {}) } as CreateWizardOptions,
missingRequired: title === undefined || options.team === undefined,
});
// For id positionals: use EMPTY_SPEC + `positional: { name, value, picker }`.
```

`maybeCollectInteractive` returns inputs **untouched** when `shouldPrompt`
(`gating.ts`) says no — that gate fires only on a real TTY with `-i` explicit or a
required arg missing, and is suppressed by non-TTY, `CI`/`LINEARIS_NO_INTERACTIVE`,
`--no-interactive`, `--compact`, or `--fields`.

### Reuse, don't reinvent

- Choices come from `src/common/interactive/choices.ts` — loaders import **list
services** via `ctx.gql`, not resolvers (`teamChoices`, `assigneeChoices`,
`projectChoices`, `milestoneChoices`, `statusChoices`, `priorityChoices`, …). The
one exception is `estimateChoices`, which reads a team's estimate scale via a
resolver under a documented `ARCHITECTURAL EXCEPTION` — follow that pattern only
when no list service exposes the data.
- Wrap with `withNoneChoice`/`optionalChoices` for escapable/optional fields; gate
cross-field fields with `when(draft)` + lazy `choices(ctx, draft)` (team before
cycle/status; project before milestone).
- Pickers: `makeChoicePicker` for a flat select; `makeDiscussionPickers`
(`discussion-pickers.ts`) for thread/reply selection. Entity-picker commands seed
the resolved parent (e.g. the issue's team) into the draft before the wizard.

### Rules

- Never `console.log` or prompt on stdout; never bypass `shouldPrompt` or weaken its
gates. Choice loaders read list services, not resolvers (see the `estimateChoices`
exception above).
- Cancellation is handled centrally (`InteractiveCancelledError` →
`{"error":"INTERACTIVE_CANCELLED"}` on stderr, exit 1) — don't catch it in commands.
- `tests/unit/interactive/coverage-sweep.test.ts` fails CI if a create/update ships
without a matching spec or an entity-id positional is left required. Run `npm test`
after adding a command.

## File Map

```
Expand All @@ -202,15 +270,38 @@ src/
services/ # business logic (GraphQL CRUD)
commands/ # CLI definitions (Commander.js)
common/ # context, output, errors, types, auth, usage
interactive/ # descriptor-driven prompt engine (input only)
gql/ # GENERATED — do not edit
graphql/
queries/ # .graphql query definitions
mutations/ # .graphql mutation definitions
tests/
unit/ # mirrors src/ structure
integration/ # CLI integration tests (need API token)
skills/
linearis/SKILL.md # agent skill teaching the CLI (has its own version)
.claude-plugin/ # Claude Code plugin + marketplace manifests (versioned)
```

## Skill & Plugin Versioning

The agent skill and Claude Code plugin (`skills/linearis/SKILL.md`,
`.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`) carry a
**SemVer** version that is **independent of the date-based npm package version**
(`package.json`). Consumers pull skill/plugin updates by this version, so it must
move whenever the shipped content changes.

**When you edit any skill or plugin file, bump the version in the same change:**

- Keep all four version fields in lockstep — `metadata.version` in `SKILL.md`,
`version` in `plugin.json`, and both `metadata.version` and `plugins[0].version`
in `marketplace.json`.
- **patch** (`1.0.0` → `1.0.1`) for wording, guidance, or fixes to existing
content; **minor** for new capability or materially new instructions; **major**
for breaking changes to what agents rely on.
- Commit the bump as its own `chore(skill):` commit (or fold it into the content
commit), separate from the date-based release flow — never touch `CHANGELOG.md`.

## Verification Checklist

Before claiming work is complete, run:
Expand Down
165 changes: 74 additions & 91 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,100 +12,95 @@

</div>

Linearis is a command-line interface for Linear that speaks **JSON only**. It resolves human-friendly IDs (like `ENG-42` or a team name) to UUIDs for you, and exposes a two-tier `usage` system so an agent can discover exactly the commands it needs without loading the whole API surface into context.
Linearis is a command-line interface for Linear that speaks **JSON only**. It resolves human-friendly IDs (`ENG-42`, a team name) to UUIDs for you, prompts interactively when a human is at the keyboard, and stays out of the way — pure JSON on stdout — when a script or an agent is driving.

<div align="center">

<em>The same task — creating an issue — from the two audiences Linearis serves.</em>

**A human, interactively** — searchable pickers, multiselect, and a date picker fill the gaps:

![Interactive issues create wizard](docs/assets/issue-create-interactive.svg)

**An agent (Claude Code)** — the `linearis` skill drives discover-then-act, then creates the issue:

![Claude Code creating an issue via the linearis skill](docs/assets/issue-create-agent.svg)

</div>

## Quick start

```bash
npm install -g linearis
linearis auth login
npm install -g linearis # requires Node.js >= 22
linearis auth login # opens Linear, stores an encrypted token
linearis issues list --limit 10
```

## Why Linearis?
The `linearis` command is canonical; `linear` is a fully supported alias.

The official Linear MCP works well, but it costs ~13k tokens just by being connected — before an agent does anything. Linearis takes a different approach: agents discover capabilities on demand through a two-tier usage system.
## Why Linearis

- `linearis usage` — a compact overview of every domain (~200 tokens).
- `linearis <domain> usage` — the full reference for one domain (~300–500 tokens).
The official Linear MCP works well, but it costs ~13k tokens just by being connected — before an agent does anything. Linearis flips that: agents discover capabilities on demand through a two-tier `usage` system, and a typical interaction costs **~500–700 tokens** instead of ~13k.

A typical agent interaction costs **~500–700 tokens** of context instead of ~13k. The agent pays only for what it uses, one domain at a time.
| | Linearis | Linear MCP |
|---|---|---|
| Context cost | ~500–700 tokens per interaction | ~13k tokens on connect |
| Coverage | Common operations (issues, discussions, cycles, docs, files) | Full Linear API |
| Output | JSON via stdout | Tool-call responses |
| Setup | `npm install -g linearis` + Bash tool | MCP server connection |

> [!NOTE]
> The trade-off is coverage. Linearis focuses on the operations that matter for day-to-day work — issues, discussions, cycles, projects, documents, and files. For custom workflows, integrations, or workspace settings, the MCP is the better choice.
> The trade-off is coverage. Linearis focuses on day-to-day work — issues, discussions, cycles, projects, documents, and files. For custom workflows, integrations, or workspace settings, the MCP is the better choice.

## Features

- **JSON-only output** — pipe into `jq`, no parsing of tables or prose.
- **Smart ID resolution** — pass `ENG-42`, a team name, or a UUID interchangeably.
- **Two-tier discovery** — self-documenting `usage` commands keep agent context small.
- **Interactive when it helps** — pickers and field wizards fill missing input in a TTY; hard-gated off for pipes, CI, and agents.
- **Discussion threads** — first-class root/reply modeling on issues.
- **File attachments** — upload and download with signed URLs.
- **Broad domain coverage** — issues, projects, cycles, milestones, initiatives, documents, labels, teams, users, and more.

## Installation

```bash
npm install -g linearis
```

Requires **Node.js ≥ 22**. The `linearis` command is canonical; `linear` is a fully supported alias that runs the same CLI.

## Authentication

The interactive flow opens Linear in your browser, walks you through creating an API key, and stores it encrypted in `~/.linearis/token`:

```bash
linearis auth login
```

Or provide a token directly:

```bash
linearis --api-token <token> issues list # via flag
LINEAR_API_TOKEN=<token> linearis issues list # via environment variable
```

Token resolution order: `--api-token` flag → `LINEAR_API_TOKEN` env → `~/.linearis/token` → `~/.linear_api_token` (deprecated).

## Usage

All output is JSON. Start with discovery, then act.
Every command returns JSON. Agents follow a **discover-then-act** loop; humans can jump straight to commands.

```bash
# Discover what's available (~200 tokens)
# 1. Discover — a compact overview of every domain (~200 tokens)
linearis usage

# Drill into one domain for its full command reference
# 2. Drill down — the full reference for one domain (~300–500 tokens)
linearis issues usage

# List and search
# 3. Act
linearis issues list --limit 10
linearis issues search "authentication bug"

# Create an issue
linearis issues create "Fix login flow" --team Platform --priority 2

# Read an issue (includes embeds with signed download URLs)
linearis issues read ENG-42
linearis issues read ENG-42 # includes embeds with signed download URLs
```

For the complete reference of every command and flag, run `linearis <domain> usage`.

### Discussions
### Interactive prompts

Discussions are modeled as root threads with replies, rather than a flat comment list:
In a real terminal, Linearis prompts for missing input instead of erroring (see the demo above). The final stdout is always the same JSON; prompts are drawn on stderr.

```bash
# Start a discussion thread on an issue
linearis issues discuss ENG-42 --body "Investigating this now"
linearis issues create # auto-launches a wizard (TTY only)
linearis issues create "Fix login" -i # force interactive; flags still win
linearis issues create "Fix login" --team ENG --no-interactive # opt out
```

Prompts are hard-gated off whenever stdin/stdout is not a TTY, `CI` or `LINEARIS_NO_INTERACTIVE` is set, or `--no-interactive` / `--compact` / `--fields` is used — so pipes and agents never hang and stdout stays pure JSON.

# List root discussion threads for an issue
linearis issues discussions ENG-42
### Discussions

# List replies in one root thread
linearis issues replies <root-thread-id>
Discussions are modeled as root threads with replies, not a flat comment list:

# Reply to a thread (use a root discussion thread ID, not a reply ID)
linearis issues reply <root-thread-id> --body "I found the root cause"
```bash
linearis issues discuss ENG-42 --body "Investigating this now" # start a thread
linearis issues discussions ENG-42 # list root threads
linearis issues replies <root-thread-id> # list replies
linearis issues reply <root-thread-id> --body "Found the cause" # reply to a thread
```

### Domains
Expand All @@ -125,55 +120,45 @@ linearis issues reply <root-thread-id> --body "I found the root cause"
| `users` | Workspace members and assignees |
| `auth` | Authenticate with the Linear API |

## AI agent integration

Linearis is structured around a **discover-then-act** pattern that matches how agents work:

1. **Discover** — `linearis usage` returns a compact overview of all domains. The agent reads it once.
2. **Drill down** — `linearis <domain> usage` gives the full reference for a single domain. The agent loads only what it needs.
3. **Execute** — every command returns structured JSON. No table or prose parsing.

The agent never loads the full API surface into context — it pays for what it uses, one domain at a time.
Run `linearis <domain> usage` for the complete command and flag reference.

### Linearis vs. Linear MCP
## Authentication

| | Linearis | Linear MCP |
|---|---|---|
| Context cost | ~500–700 tokens per interaction | ~13k tokens on connect |
| Coverage | Common operations (issues, discussions, cycles, docs, files) | Full Linear API |
| Output | JSON via stdout | Tool-call responses |
| Setup | `npm install -g linearis` + Bash tool | MCP server connection |
`linearis auth login` is the easy path. To supply a token directly:

Use Linearis when token efficiency matters and you work primarily with issues and related data. Use the MCP when you need full API coverage or tight tool-call integration.
```bash
linearis --api-token <token> issues list # via flag
LINEAR_API_TOKEN=<token> linearis issues list # via environment variable
```

### Agent skill
Resolution order: `--api-token` → `LINEAR_API_TOKEN` → `~/.linearis/token` → `~/.linear_api_token` (deprecated).

Linearis ships an agent skill (following the [agentskills.io](https://agentskills.io) standard) so your agent knows how to use it — no prompt to paste. The skill preflights the install, advisory-checks for updates, then follows the discover-then-act protocol above.
## For AI agents

**Any harness (recommended)** — Vercel's skills CLI installs into the right place for 70+ agents and lists it on [skills.sh](https://skills.sh):
Linearis ships an agent skill (following the [agentskills.io](https://agentskills.io) standard) so your agent knows the discover-then-act protocol with no prompt to paste. It preflights the install and advisory-checks for updates.

```bash
npx skills add linearis-oss/linearis
npx skills add linearis-oss/linearis # any harness — installs for 70+ agents
```

**Claude Code** — native plugin:
<details>
<summary>Other harnesses</summary>

```
/plugin marketplace add linearis-oss/linearis
/plugin install linearis@linearis
```
- **Claude Code** — native plugin:
```
/plugin marketplace add linearis-oss/linearis
/plugin install linearis@linearis
```
- **OpenAI Codex** — `npx skills add linearis-oss/linearis` installs to `~/.agents/skills/`; invoke with `/skills` or `$`.
- **pi** — `npx skills add linearis-oss/linearis` (or drop `skills/linearis/` into `.pi/skills/`); invoke `/skill:linearis`.
- **Google Antigravity** — `npx skills add linearis-oss/linearis` installs to `.agents/skills/`; auto-discovered.

**OpenAI Codex** — `npx skills add linearis-oss/linearis` installs to `~/.agents/skills/`; invoke with `/skills` or `$`.

**pi** — `npx skills add linearis-oss/linearis` (or drop `skills/linearis/` into `.pi/skills/`); invoke `/skill:linearis`.

**Google Antigravity** — `npx skills add linearis-oss/linearis` installs to `.agents/skills/`; auto-discovered from the skill list.
</details>

## Documentation

- [MIGRATION_2026.4.9.md](MIGRATION_2026.4.9.md) — migrating from the deprecated `comments` domain to discussions (v2026.4.9).
- [`docs/`](docs/) — architecture, development, testing, and build-system references.
- [`docs/ci-run-model.md`](docs/ci-run-model.md) — the authoritative CI/release trigger matrix.
- [MIGRATION_2026.4.9.md](MIGRATION_2026.4.9.md) — migrating from the deprecated `comments` domain to discussions.
- [CONTRIBUTING.md](CONTRIBUTING.md) — contributor guidelines.
- [SECURITY.md](SECURITY.md) — how to report security issues.

Expand All @@ -187,6 +172,4 @@ Made with [contrib.rocks](https://contrib.rocks).

## License

[MIT](LICENSE.md)

This project is neither affiliated with nor endorsed by Linear.
[MIT](LICENSE.md) — this project is neither affiliated with nor endorsed by Linear.
Loading
Loading