@kb1slnlabs/dsh-linear 0.0.0-stage → 0.2.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.
- package/LICENSE +21 -0
- package/README.md +408 -2
- package/THIRD_PARTY_NOTICES.md +19 -0
- package/cordis.patch.yml +19 -0
- package/index.js +1460 -0
- package/package.json +40 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 KB1SLN-Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,409 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @kb1slnlabs/dsh-linear
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Linear issue-tracking tools for the DeepSeek Harness. A Cordis function/namespace
|
|
4
|
+
plugin (`export { name, inject, apply }`, no default export) that registers
|
|
5
|
+
model-facing tools through `ctx.tools.register`.
|
|
6
|
+
|
|
7
|
+
It exposes **one tool surface**: 26 hand-written tools over the **Linear GraphQL
|
|
8
|
+
API** (`https://api.linear.app/graphql`), authenticated with a personal API token.
|
|
9
|
+
|
|
10
|
+
**There is no MCP in this package — at all.** It does not host an MCP server, it
|
|
11
|
+
does not act as an MCP client, it never connects to `mcp.linear.app`, and it has
|
|
12
|
+
no MCP configuration, dependency, or test. The 26 `linear_*` tools below are the
|
|
13
|
+
entire surface; nothing else is registered under any condition.
|
|
14
|
+
|
|
15
|
+
> **Scope honesty:** 25 of these tools are a fixed, hand-modelled subset of what
|
|
16
|
+
> the Linear GraphQL API can do. They cover issues, comments, relations, teams,
|
|
17
|
+
> states, labels, projects, cycles and users — and nothing beyond that (no
|
|
18
|
+
> attachments, no documents/projects search, no webhooks, no auth/user-management
|
|
19
|
+
> administration, no bulk operations). The set is not auto-discovered and does not
|
|
20
|
+
> grow on its own: if you need a capability outside the table below, it is not
|
|
21
|
+
> there. The 26th tool, [`linear_graphql`](#direct-graphql-escape-hatch-linear_graphql),
|
|
22
|
+
> is the deliberate escape hatch for exactly that case — but what it can reach is
|
|
23
|
+
> bounded by Linear's own API and by your key's permissions, so it extends
|
|
24
|
+
> coverage, it does not guarantee parity. See [Limitations](#limitations).
|
|
25
|
+
|
|
26
|
+
> Status: this is **version 0.2.0** of the standalone implementation — the
|
|
27
|
+
> GitHub release for it is
|
|
28
|
+
> [KB1SLN-Labs/dsh-linear/releases/tag/v0.2.0](https://github.com/KB1SLN-Labs/dsh-linear/releases/tag/v0.2.0),
|
|
29
|
+
> and the package is published on npm as `@kb1slnlabs/dsh-linear`. The earlier
|
|
30
|
+
> **0.1.0** release, also on the public GitHub repository `KB1SLN-Labs/dsh-linear`
|
|
31
|
+
> and on npm, shipped with an MCP bridge to Linear's hosted MCP server; that
|
|
32
|
+
> bridge is gone from 0.2.0 (see **There is no MCP in this package — at all.**
|
|
33
|
+
> above and [Limitations](#limitations)), so do not treat 0.1.0 as this package.
|
|
34
|
+
> The offline suite described under
|
|
35
|
+
> [Testing](#testing) is mock-tested (loopback only), except
|
|
36
|
+
> that it runs a live schema check when `LINEAR_API_KEY` is set;
|
|
37
|
+
> `test-loader.mjs` is a separate loader smoke test, not part of `npm test`.
|
|
38
|
+
|
|
39
|
+
## Tools
|
|
40
|
+
|
|
41
|
+
### GraphQL tools (`linear_*`) — 26 registrations
|
|
42
|
+
|
|
43
|
+
| Tool | Purpose |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `linear_issue_list` | List issues (filter by team key, state name, free-text search; `limit` default 20, max 50; cursor pagination via `after` / `pageInfo`) |
|
|
46
|
+
| `linear_issue_get` | One issue by id or identifier (e.g. `APT-123`), incl. relations |
|
|
47
|
+
| `linear_issue_create` | Create an issue in a team |
|
|
48
|
+
| `linear_issue_update` | Update title, description, state, assignee, labels, priority |
|
|
49
|
+
| `linear_issue_archive` | Archive an issue by id or identifier |
|
|
50
|
+
| `linear_issue_unarchive` | Unarchive a previously archived issue by id or identifier |
|
|
51
|
+
| `linear_comment_add` | Add a comment to an issue |
|
|
52
|
+
| `linear_comment_update` | Update a comment body (comment UUID from `linear_comment_list`) |
|
|
53
|
+
| `linear_comment_delete` | Delete a comment (comment UUID) — destructive |
|
|
54
|
+
| `linear_comment_list` | List comments on an issue (newest first; cursor pagination via `after` / `pageInfo`) |
|
|
55
|
+
| `linear_relation_add` | Link a related issue (`related` / `blockedBy` / `blocks` / `parent` / `child`) |
|
|
56
|
+
| `linear_team_list` | List teams (`after` / `pageInfo`; page size fixed at 100, no `limit` argument) |
|
|
57
|
+
| `linear_team_get` | One team by id (UUID) or key |
|
|
58
|
+
| `linear_state_list` | List workflow states for a team (cursor pagination via `after` / `pageInfo`) |
|
|
59
|
+
| `linear_label_list` | List labels for a team (cursor pagination via `after` / `pageInfo`) |
|
|
60
|
+
| `linear_label_get` | One label by id (UUID) |
|
|
61
|
+
| `linear_label_create` | Create a label for a team |
|
|
62
|
+
| `linear_project_list` | List projects (`limit` default 20, max 50; cursor pagination via `after` / `pageInfo`) |
|
|
63
|
+
| `linear_project_get` | One project by id (UUID) |
|
|
64
|
+
| `linear_project_create` | Create a project (optional team keys) |
|
|
65
|
+
| `linear_project_update` | Update a project: name, description, content, icon, color, team keys, start/target date, priority |
|
|
66
|
+
| `linear_cycle_list` | List cycles for a team (`limit` default 20, max 50; cursor pagination via `after` / `pageInfo`) |
|
|
67
|
+
| `linear_cycle_get` | One cycle by id (UUID) |
|
|
68
|
+
| `linear_user_list` | List users in the workspace (`limit` default 20, max 50; cursor pagination via `after` / `pageInfo`) |
|
|
69
|
+
| `linear_user_get` | One user by id (UUID) or email |
|
|
70
|
+
| `linear_graphql` | **Escape hatch** — send an arbitrary GraphQL document (query *or* mutation) straight to the Linear API; see below |
|
|
71
|
+
|
|
72
|
+
Cursor pagination is live on **all eight** list tools — `linear_issue_list`,
|
|
73
|
+
`linear_comment_list`, `linear_team_list`, `linear_state_list`,
|
|
74
|
+
`linear_label_list`, `linear_project_list`, `linear_cycle_list` and
|
|
75
|
+
`linear_user_list`. Each accepts an optional `after` cursor (the previous page's
|
|
76
|
+
`pageInfo.endCursor`) and returns `pageInfo { hasNextPage, endCursor }`; the
|
|
77
|
+
cursor is always sent as a GraphQL variable (`$after: String`), never
|
|
78
|
+
interpolated into the document, and a server that reports no `pageInfo` yields
|
|
79
|
+
`pageInfo: null` rather than an invented value. Page sizes are unchanged: `limit`
|
|
80
|
+
defaults to 20 and is clamped to a maximum of **50** wherever it exists
|
|
81
|
+
(`issue_list`, `comment_list`, `project_list`, `cycle_list`, `user_list`), and
|
|
82
|
+
`linear_team_list` has no `limit` argument at all — its page size stays fixed at
|
|
83
|
+
100. `state_list` and `label_list` likewise take no `limit`; they are paginated
|
|
84
|
+
purely through `after`. See [Limitations](#limitations).
|
|
85
|
+
|
|
86
|
+
### Direct GraphQL escape hatch (`linear_graphql`)
|
|
87
|
+
|
|
88
|
+
One tool is not a hand-modelled endpoint. `linear_graphql` takes the caller's own
|
|
89
|
+
GraphQL document and forwards it verbatim to the **same** fixed endpoint
|
|
90
|
+
(`https://api.linear.app/graphql`) with the **same** configured key, through the
|
|
91
|
+
same client and the same `{ "error": … }` wrapper every other tool uses. It adds
|
|
92
|
+
no endpoint, no dependency, no setting, and no credential of its own.
|
|
93
|
+
|
|
94
|
+
| Argument | Type | Notes |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `query` | string, **required** | The GraphQL document — a query or a mutation. Must be non-blank. Sent verbatim; the tool adds, wraps, or normalises nothing. |
|
|
97
|
+
| `variables` | object, optional | GraphQL variable values keyed by name (e.g. `{ "id": "<uuid>" }`). Prefer variables over splicing values into the document text. |
|
|
98
|
+
|
|
99
|
+
The result is the server's raw `data`, JSON-stringified as `{ "data": … }`: no
|
|
100
|
+
projection, no field curation, no `pageInfo` normalisation — the document you
|
|
101
|
+
write decides the shape, including whether you ask for `pageInfo { hasNextPage
|
|
102
|
+
endCursor }` yourself.
|
|
103
|
+
|
|
104
|
+
Both queries and mutations are allowed, and that is the point. A mutation
|
|
105
|
+
through this tool really writes, and what it may read or change is governed
|
|
106
|
+
entirely by the permissions attached to your Linear API key — the same authority
|
|
107
|
+
every other tool here already runs under. There is no extra allow-list, no
|
|
108
|
+
read-only mode, and no dry-run: if your key can do it and the Linear schema
|
|
109
|
+
accepts the document, it happens.
|
|
110
|
+
|
|
111
|
+
**Coverage here depends on Linear's API, not on this package.** The escape hatch
|
|
112
|
+
can only reach what `api.linear.app` currently exposes to your key — fields,
|
|
113
|
+
types, and mutations Linear defines today, subject to their own deprecations,
|
|
114
|
+
rate limits, and workspace scoping. It is *not* named-tool parity: there are no
|
|
115
|
+
guarantees that a capability exists, that it stays stable across Linear releases,
|
|
116
|
+
that the plugin's live schema guard covers it, or that the offline mock suite
|
|
117
|
+
models it (the suite proves the plumbing — document pass-through, variables as
|
|
118
|
+
variables, and error handling — not any specific user-written document). The 25
|
|
119
|
+
named tools carry curated inputs, identifier resolution, and projected outputs;
|
|
120
|
+
this one deliberately carries none of that.
|
|
121
|
+
|
|
122
|
+
## Configuration
|
|
123
|
+
|
|
124
|
+
One credential, resolved by the plugin at apply time:
|
|
125
|
+
|
|
126
|
+
| Setting | Env var | Plugin config | Used for | Header sent |
|
|
127
|
+
|---|---|---|---|---|
|
|
128
|
+
| GraphQL key | `LINEAR_API_KEY` | `apiKey` | all 26 `linear_*` tools, incl. `linear_graphql` | `Authorization: <raw key>` — **not** `Bearer` |
|
|
129
|
+
|
|
130
|
+
```powershell
|
|
131
|
+
# PowerShell (current session)
|
|
132
|
+
$env:LINEAR_API_KEY = "li_api_..."
|
|
133
|
+
|
|
134
|
+
# or persist for the user
|
|
135
|
+
setx LINEAR_API_KEY "li_api_..."
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The `dsh` launcher inherits the parent environment, so any harness started after a
|
|
139
|
+
variable is set will see it. A config `apiKey` in the patch row wins over the env
|
|
140
|
+
var; when it is empty or absent, the env var is used. With neither set, every tool
|
|
141
|
+
returns `{ "error": "Linear API key not configured: …" }` instead of throwing.
|
|
142
|
+
|
|
143
|
+
That is the whole config object:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
const Config = z.object({ apiKey: z.string().default('') })
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
There are no other settings and no other environment variables. The plugin reads
|
|
150
|
+
no MCP-related setting, opens no non-GraphQL connection, and an `mcp` block left in
|
|
151
|
+
an old profile's config is simply ignored (the schema declares no such field).
|
|
152
|
+
|
|
153
|
+
The repo's own `cordis.patch.yml` inserts the plugin with **no config at all** — it
|
|
154
|
+
relies on the environment variables.
|
|
155
|
+
|
|
156
|
+
Override env vars used to pin dependency resolution (mainly for offline tests):
|
|
157
|
+
`DSH_LINEAR_TOOLS`, `DSH_LINEAR_SCHEMA`.
|
|
158
|
+
|
|
159
|
+
## Install
|
|
160
|
+
|
|
161
|
+
This is **version 0.2.0** of the standalone implementation; its release is
|
|
162
|
+
[KB1SLN-Labs/dsh-linear/releases/tag/v0.2.0](https://github.com/KB1SLN-Labs/dsh-linear/releases/tag/v0.2.0),
|
|
163
|
+
and the tarball attached to that release — `kb1slnlabs-dsh-linear.tgz` — is the
|
|
164
|
+
pinned artifact for route 4 below. Four install routes:
|
|
165
|
+
|
|
166
|
+
```powershell
|
|
167
|
+
# 0. install this plugin's own dependencies (local checkout route only)
|
|
168
|
+
npm install
|
|
169
|
+
|
|
170
|
+
# 1. local link with an absolute path (working checkout, edits picked up in place)
|
|
171
|
+
dsh plugin --profile web add link:<absolute-path-to-this-directory>
|
|
172
|
+
|
|
173
|
+
# 2. from the public GitHub repository (default branch — unpinned)
|
|
174
|
+
dsh plugin --profile web add github:KB1SLN-Labs/dsh-linear
|
|
175
|
+
|
|
176
|
+
# 3. from npm, by package name (latest published version)
|
|
177
|
+
dsh plugin --profile web add @kb1slnlabs/dsh-linear
|
|
178
|
+
|
|
179
|
+
# 4. version-pinned release asset — download the tarball attached to the v0.2.0
|
|
180
|
+
# GitHub release, then install it from disk:
|
|
181
|
+
# https://github.com/KB1SLN-Labs/dsh-linear/releases/download/v0.2.0/kb1slnlabs-dsh-linear.tgz
|
|
182
|
+
dsh plugin --profile web add file:./kb1slnlabs-dsh-linear.tgz
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Route 1 (`link:`) points pnpm at the directory rather than copying it, so the profile
|
|
186
|
+
picks up edits in place. Use an **absolute** path — that is what this repo's own install
|
|
187
|
+
used, and it is what makes the spec unambiguous regardless of where `dsh plugin` runs
|
|
188
|
+
from. Routes 2–4 install a copy instead, so later source edits do not leak into the
|
|
189
|
+
profile. Only route 4 pins an exact version: `kb1slnlabs-dsh-linear.tgz` is the fixed
|
|
190
|
+
release-asset name for the v0.2.0 GitHub release — renamed from the output of
|
|
191
|
+
`npm pack`, not the filename npm itself generates. Route 2 follows the repository's
|
|
192
|
+
default branch and route 3 follows npm's `latest`, so neither is guaranteed to install
|
|
193
|
+
0.2.0 once newer work or releases exist; use route 4 when reproducibility matters.
|
|
194
|
+
|
|
195
|
+
The clean scratch profile behind the verification described under
|
|
196
|
+
[Testing](#testing) was installed through the **DSH plugin manager**, not by hand.
|
|
197
|
+
Adding the package also adds `@kb1slnlabs/dsh-linear` to the
|
|
198
|
+
profile's `dsh.profile.bundles` layer list, because its manifest declares
|
|
199
|
+
`dsh.bundle.patch`.
|
|
200
|
+
|
|
201
|
+
To add it as a plain dependency instead of a link, use `file:<absolute-path>` or a
|
|
202
|
+
packaged tarball (`npm pack`, then `dsh plugin --profile web add file:<path-to-tgz>`) —
|
|
203
|
+
that is how the scratch profile used by [Testing](#testing) was built.
|
|
204
|
+
|
|
205
|
+
Peer dependencies `@deepseek-ai/dsh-tools` and `@deepseek-ai/cordis`, plus the
|
|
206
|
+
`@deepseek-ai/schemastery` this package depends on, normally resolve from the **dsh
|
|
207
|
+
install's own** `node_modules`. The plugin resolves each dependency as a bare specifier
|
|
208
|
+
first, then from an anchor derived portably from `process.execPath` (the npm/pnpm global
|
|
209
|
+
prefix), then `%APPDATA%/npm/node_modules` on Windows or `/usr/local/lib/node_modules` /
|
|
210
|
+
`/usr/lib/node_modules` on POSIX.
|
|
211
|
+
|
|
212
|
+
The `@deepseek-ai/dsh-tools` this package pins (`0.1.5-rc.2`) was verified against host
|
|
213
|
+
**0.2.1-rc.3**. In a base-only scratch profile it can log **missing-peer warnings** —
|
|
214
|
+
that is expected: the host supplies those services, and the pinned copy exists so the
|
|
215
|
+
plugin resolves its own peer at all. Do not read such a warning as a failure.
|
|
216
|
+
|
|
217
|
+
## Testing
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
node test.js # module surface (name/inject/apply/Config), 26 tool
|
|
221
|
+
# registrations, an `mcp` config proven inert (no extra
|
|
222
|
+
# tools, no log lines, no effects, no connection), ~183
|
|
223
|
+
# GraphQL calls against a loopback mock that mirrors the
|
|
224
|
+
# real Linear schema (flat UUID inputs, plural-field
|
|
225
|
+
# resolution, raw-key auth header) — the exact call count
|
|
226
|
+
# is printed by the suite rather than asserted — cursor
|
|
227
|
+
# pagination across all eight direct list tools
|
|
228
|
+
# (issue, comment, team, state, label, project, cycle,
|
|
229
|
+
# user), the five direct
|
|
230
|
+
# write tools (project update, comment update/delete, issue
|
|
231
|
+
# archive/unarchive) incl. unresolved identifiers and
|
|
232
|
+
# success:false payloads, the linear_graphql escape hatch
|
|
233
|
+
# (query and mutation documents, variables as variables,
|
|
234
|
+
# invalid arguments, GraphQL and HTTP errors), negative
|
|
235
|
+
# cases; and, when LINEAR_API_KEY is set, the live
|
|
236
|
+
# introspection guard
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`npm test` runs exactly that. It needs no key and makes no network call **except**
|
|
240
|
+
the introspection guard below, which runs only when `LINEAR_API_KEY` is present.
|
|
241
|
+
|
|
242
|
+
`test.js` additionally carries a **live introspection guard** that runs only when
|
|
243
|
+
`LINEAR_API_KEY` is set: it introspects the real Linear schema and asserts every input
|
|
244
|
+
field, query field, mutation field, mutation argument, and payload field the plugin emits
|
|
245
|
+
actually exists. Without a key it prints
|
|
246
|
+
`skip - introspection guard`. So in an environment with no `LINEAR_API_KEY` the suite
|
|
247
|
+
passes with that guard skipped, but **with a key set, `test.js` does use live network** —
|
|
248
|
+
it sends a real introspection request to `https://api.linear.app/graphql`, and a schema
|
|
249
|
+
mismatch makes the run throw rather than skip.
|
|
250
|
+
|
|
251
|
+
### `test-loader.mjs` — optional loader smoke test (not part of `npm test`)
|
|
252
|
+
|
|
253
|
+
Exercises the **real plugin loader and the real host tool runtime**: it boots a Cordis
|
|
254
|
+
`Context`, attaches the host's real `SystemPrompt` and `ToolRuntime` services (imported
|
|
255
|
+
from the dsh installation, not stubbed), runs `cordis-plugin-loader`, installs this
|
|
256
|
+
plugin into that context through the profile, asserts the entry got a live fiber and
|
|
257
|
+
that exactly the 26 `linear_*` names are known to the runtime — then disposes the fiber
|
|
258
|
+
and asserts all 26 are gone. It carries a 15-second step deadline: before each phase it
|
|
259
|
+
checks whether 15 seconds have elapsed and, if so, fails with
|
|
260
|
+
`15 second bound exceeded at: <label>`. That deadline is a progress budget between steps,
|
|
261
|
+
**not** a hard timeout — it cannot interrupt an asynchronous call that never settles.
|
|
262
|
+
|
|
263
|
+
Both variables are **required**; a missing or malformed one fails immediately with an
|
|
264
|
+
actionable message rather than an import error:
|
|
265
|
+
|
|
266
|
+
| Variable | Meaning | Accepted form |
|
|
267
|
+
|---|---|---|
|
|
268
|
+
| `DSH_ROOT` | the dsh installation directory — the one holding `node_modules/@deepseek-ai/cordis`, from which `cordis`, `cordis-plugin-loader`, `dsh-system-prompt` and `dsh-tools` are resolved | absolute filesystem path (a trailing slash is added for you) |
|
|
269
|
+
| `LINEAR_TEST_PROFILE` | the profile directory to load the plugin against — it must already have this package installed as a dependency/bundle layer | absolute filesystem path **or** a `file:` URL; converted with `pathToFileURL` plus a trailing slash so it is a directory URL, and checked for existence |
|
|
270
|
+
|
|
271
|
+
A non-`file:` URL (`http:`, `npm:`, …) is rejected. Relative paths are not accepted —
|
|
272
|
+
there is no sensible anchor for them here.
|
|
273
|
+
|
|
274
|
+
```powershell
|
|
275
|
+
$env:DSH_ROOT = "C:/Users/<you>/AppData/Roaming/npm/node_modules/@deepseek-ai/dsh"
|
|
276
|
+
$env:LINEAR_TEST_PROFILE = "<dsh-home>/profiles/<profile>" # or file:///<same path>
|
|
277
|
+
node test-loader.mjs
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
The suite deletes `LINEAR_API_KEY`, `LINEAR_MCP_TOKEN`, `LINEAR_APIKEY` and
|
|
281
|
+
`LINEAR_KEY` from its own environment before loading, so the plugin it installs cannot
|
|
282
|
+
pick up a live credential. (It clears the MCP variable for cleanliness only — this
|
|
283
|
+
plugin no longer reads it.) That clearing is scoped to this process; `test.js` behaves
|
|
284
|
+
differently, deliberately running live against `api.linear.app` when a real key is
|
|
285
|
+
present (see above).
|
|
286
|
+
|
|
287
|
+
**What this does not cover:** it is not a full headless boot. No LLM, session log,
|
|
288
|
+
filesystem sandbox, or app entry tree is started — only the loader plus those two host
|
|
289
|
+
services inside a bare `Context`. And it never contacts Linear: no GraphQL call is made.
|
|
290
|
+
The scratch profile it runs against is a throwaway directory, not your day-to-day
|
|
291
|
+
profile.
|
|
292
|
+
|
|
293
|
+
### What is verified vs. what is not
|
|
294
|
+
|
|
295
|
+
- **Verified (mock-tested):** what `test.js` asserts — registration of exactly the 26
|
|
296
|
+
tools, GraphQL query/mutation shape, the raw-key auth header form, identifier
|
|
297
|
+
resolution, `{ error }` payloads, cursor pagination on all eight direct list
|
|
298
|
+
tools (first-page `pageInfo`, `after` passed only as a `$after` variable, blank
|
|
299
|
+
and non-string cursors treated as no cursor, a bad cursor surfacing as a clean
|
|
300
|
+
error, and a pageInfo-less server normalising to `null`), page sizes unchanged
|
|
301
|
+
(`limit` default 20 clamped to max 50; `team_list` still `first: 100` with no
|
|
302
|
+
`limit` parameter),
|
|
303
|
+
the five direct write tools (`projectUpdate`, `commentUpdate`, `commentDelete`,
|
|
304
|
+
`issueArchive`, `issueUnarchive`) — including that an unresolved project name or issue
|
|
305
|
+
identifier never reaches a mutation and that a `success: false` payload surfaces as a
|
|
306
|
+
clean error — the `linear_graphql` escape hatch (a query document and a mutation
|
|
307
|
+
document forwarded verbatim, caller variables arriving as variables rather than query
|
|
308
|
+
text, blank/non-string/missing `query` and non-object `variables` rejected before any
|
|
309
|
+
request, GraphQL `errors[]` and non-2xx responses surfacing through the same
|
|
310
|
+
`{ error }` wrapper; all against loopback, so no real write happens) — and that an
|
|
311
|
+
`mcp` config block is inert (registers nothing, logs nothing, calls no effect).
|
|
312
|
+
- **Verified against real host services (not mocks):** `test-loader.mjs` loads this
|
|
313
|
+
package through the real plugin loader into a real `ToolRuntime`. The scratch profile
|
|
314
|
+
behind that run was installed via the **DSH plugin manager** and verified on **desktop
|
|
315
|
+
runtime 0.2.1-rc.3** with the Cordis loader plus `ToolRuntime` — a bare host context,
|
|
316
|
+
**not** a full desktop app boot: no LLM turn, session log, sandbox, or app entry tree
|
|
317
|
+
was started.
|
|
318
|
+
- **Verified (CLI install smoke, separate from the line above):** a clean scratch
|
|
319
|
+
profile based on the **shipped headless template** took the **packed** plugin through
|
|
320
|
+
`dsh plugin`, and `dsh --profile <scratch> --help` exited **0** on **Windows CLI
|
|
321
|
+
0.1.5-rc.2**. That is an install-and-launch smoke check only: no model turn and no
|
|
322
|
+
full desktop GUI boot were performed, and it says nothing about the loader/`ToolRuntime`
|
|
323
|
+
run above, which was on desktop runtime 0.2.1-rc.3.
|
|
324
|
+
- **Verified against live services:** the Linear GraphQL introspection guard in `test.js`
|
|
325
|
+
has run with a real key and passed — the schema fields this plugin emits, including the
|
|
326
|
+
five direct-write mutations, their arguments, and their payload shapes (`ProjectPayload`,
|
|
327
|
+
`CommentPayload`, `DeletePayload`, `IssueArchivePayload`), exist.
|
|
328
|
+
- **Not verified:** no run in this repo's check has exercised a real `linear_*` **write**
|
|
329
|
+
end-to-end against `api.linear.app` — the GraphQL half is proven against a mock that
|
|
330
|
+
mirrors the introspected schema (plus the live schema-shape guard above), not against
|
|
331
|
+
real issue mutations. Nothing here was run on more than one Windows host/runtime, so
|
|
332
|
+
treat other platforms as untested.
|
|
333
|
+
|
|
334
|
+
## Limitations
|
|
335
|
+
|
|
336
|
+
Stated plainly, because 25 of the tools above are a fixed hand-modelled subset:
|
|
337
|
+
|
|
338
|
+
- **Pagination covers the list tools but not the page sizes.** All eight list
|
|
339
|
+
tools paginate (`after` + `pageInfo`), so nothing is cut off *silently* — you can
|
|
340
|
+
always continue from `pageInfo.endCursor`. What stays capped is the page size:
|
|
341
|
+
`limit` defaults to 20 and never exceeds **50** (`issue_list`, `comment_list`,
|
|
342
|
+
`project_list`, `cycle_list`, `user_list`), while `team_list` sends a fixed
|
|
343
|
+
`first: 100` and takes no `limit`, and `state_list`/`label_list` take no `limit`
|
|
344
|
+
at all. A single call therefore returns only one capped page; reading everything
|
|
345
|
+
means following the cursor yourself. The `_get` tools are not paginated — they
|
|
346
|
+
fetch one entity.
|
|
347
|
+
- **No search beyond free-text on issues.** `linear_issue_list`'s `search` matches
|
|
348
|
+
title/description case-insensitively; there is no project, document, or cycle search.
|
|
349
|
+
- **Create/update coverage is narrow but no longer empty.** Issues, comments, relations,
|
|
350
|
+
labels and projects can be created; issues and projects can be updated, comments edited
|
|
351
|
+
and deleted, and issues archived/unarchived. Nothing else can: there is no team or state
|
|
352
|
+
management, no cycle create, no project delete, no bulk operation, no attachment upload,
|
|
353
|
+
and no user administration. Among the named tools, `linear_comment_delete` is the
|
|
354
|
+
only destructive one and has no undo — but it is not the only destructive thing
|
|
355
|
+
this package can do: `linear_graphql` runs any mutation your key permits, so
|
|
356
|
+
operations outside the named set (deletions included) are reachable through it.
|
|
357
|
+
- **`linear_comment_add` targets issues only** — it sends `CommentCreateInput.issueId`,
|
|
358
|
+
so it cannot comment on a project, update, or document.
|
|
359
|
+
- **Fields are projected, not passed through.** The 25 named tools return the fixed
|
|
360
|
+
field sets in the GraphQL fragments (e.g. an issue's state as a name string), so
|
|
361
|
+
anything not in those fragments is simply absent from the result. `linear_graphql`
|
|
362
|
+
passes `data` through raw instead — which is why its coverage is whatever Linear's
|
|
363
|
+
API exposes, and nothing more.
|
|
364
|
+
- **`linear_graphql` is uncurated by design.** It validates that the document is a
|
|
365
|
+
non-blank string and that `variables` is an object; it does not validate the
|
|
366
|
+
document against the Linear schema, does not restrict mutations, and does not
|
|
367
|
+
rewrite anything. A malformed document fails on Linear's side as an `{ error }`
|
|
368
|
+
payload, and a valid one executes with your key's full permissions — including
|
|
369
|
+
destructive operations (deletes, archives, unarchives, and anything else the
|
|
370
|
+
Linear schema offers) that no named tool models and that this package does not
|
|
371
|
+
gate. Reach for it when the named tools fall short,
|
|
372
|
+
not instead of them.
|
|
373
|
+
- **One workspace per key.** A Linear API key is workspace-scoped; there is no
|
|
374
|
+
multi-workspace switching.
|
|
375
|
+
|
|
376
|
+
## Notes
|
|
377
|
+
|
|
378
|
+
- All GraphQL tools return JSON text; failures (a resolution error, an API error, a
|
|
379
|
+
network error) come back as `{ "error": "…" }` rather than a thrown stack trace.
|
|
380
|
+
- Issue lookups accept either a UUID or a `TEAM-123` identifier.
|
|
381
|
+
- `linear_issue_update` with `assignee: ""` unassigns.
|
|
382
|
+
- The plugin registers no prompt sections and no client UI; it is purely a tool provider.
|
|
383
|
+
|
|
384
|
+
### How identifiers are resolved (resolve-then-mutate)
|
|
385
|
+
|
|
386
|
+
Linear's GraphQL inputs are **flat UUID fields** — `IssueCreateInput` has no nested
|
|
387
|
+
`team`/`state` objects, and the singular `Query.team` field accepts only a UUID `id`.
|
|
388
|
+
So before any write, the plugin resolves human identifiers to UUIDs through the
|
|
389
|
+
**plural** query fields, which *do* accept filters:
|
|
390
|
+
|
|
391
|
+
- a team key/name → `teams(filter: { key: { eq: … } })` → `teamId`
|
|
392
|
+
- a state name → `workflowStates(filter: { team: { id: { eq: <teamId> } }, name: { eq: … } })` → `stateId`
|
|
393
|
+
- a project name → `projects(filter: { name: { eq: … } })` → the mutation's top-level `id`
|
|
394
|
+
|
|
395
|
+
If a team key/name does not resolve to exactly one team, the tool fails with a clear
|
|
396
|
+
`Team '<x>' not found` error instead of sending a malformed mutation. A project name that
|
|
397
|
+
matches more than one project is refused the same way — the resolver will not pick one of
|
|
398
|
+
several candidates and update an ambiguous target; it reports that the name matched multiple
|
|
399
|
+
projects and asks for the project UUID. An issue identifier is resolved by splitting it into
|
|
400
|
+
team key + number and querying `issues(filter: { team: { key: { eq: … } }, number: { eq: … } })`,
|
|
401
|
+
since `IssueFilter` has no `identifier` field. That lookup is archived-blind like Linear's own
|
|
402
|
+
`issues` query, **except** for `linear_issue_unarchive`, which adds `includeArchived: true` —
|
|
403
|
+
without it the one issue class that tool exists to fix would be invisible to its own lookup.
|
|
404
|
+
|
|
405
|
+
Note that the update/archive mutations do **not** put the target's UUID inside `input`:
|
|
406
|
+
`projectUpdate(id:, input:)` and `commentUpdate(id:, input:)` take it as a separate top-level
|
|
407
|
+
argument, and `commentDelete(id:)`, `issueArchive(id:)` and `issueUnarchive(id:)` take a bare
|
|
408
|
+
`id` at all — there is no `CommentDeleteInput` or archive-input type in the Linear schema. The
|
|
409
|
+
archive mutations return their issue under the payload field `entity`, not `issue`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
This plugin, `@kb1slnlabs/dsh-linear`, is licensed under the MIT license in
|
|
4
|
+
`LICENSE` (Copyright (c) 2026 KB1SLN-Labs). This file records third-party material
|
|
5
|
+
that informed or is reflected in its implementation.
|
|
6
|
+
|
|
7
|
+
The plugin's only tool surface is hand-written GraphQL calls to the Linear GraphQL
|
|
8
|
+
API. It contains no code derived from, and no closely related implementation
|
|
9
|
+
patterns copied from, any third-party package — including the DeepSeek Harness MCP
|
|
10
|
+
client bridge, which an earlier revision of this plugin hosted and which has since
|
|
11
|
+
been removed in full along with its notices.
|
|
12
|
+
|
|
13
|
+
Runtime dependencies are installed from npm under their own licenses and are not
|
|
14
|
+
reproduced here:
|
|
15
|
+
|
|
16
|
+
- `@deepseek-ai/dsh-tools` — MIT (Copyright (c) 2026 DeepSeek)
|
|
17
|
+
- `@deepseek-ai/schemastery` — MIT (Copyright (c) 2021-present Shigma)
|
|
18
|
+
|
|
19
|
+
No third-party source code is vendored or redistributed in this package.
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @kb1slnlabs/dsh-linear — profile patch layer
|
|
2
|
+
#
|
|
3
|
+
# Adds the Linear issue-tracking tool suite to a profile. The plugin registers
|
|
4
|
+
# its tools through ctx.tools.register at apply time; no further composition
|
|
5
|
+
# is needed.
|
|
6
|
+
#
|
|
7
|
+
# Config:
|
|
8
|
+
# apiKey: Linear personal API key (li_api_...). Optional — falls back to the
|
|
9
|
+
# LINEAR_API_KEY environment variable.
|
|
10
|
+
#
|
|
11
|
+
# Example row (in a profile's cordis.patch.yml):
|
|
12
|
+
# - id: linear
|
|
13
|
+
# name: '@kb1slnlabs/dsh-linear'
|
|
14
|
+
# config:
|
|
15
|
+
# apiKey: li_api_...
|
|
16
|
+
|
|
17
|
+
- insert:
|
|
18
|
+
- id: linear
|
|
19
|
+
name: '@kb1slnlabs/dsh-linear'
|