@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 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
- # Temporary Holding Version
1
+ # @kb1slnlabs/dsh-linear
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.
@@ -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'