@enrichlayer/el-linear 1.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 +219 -0
- package/claude-skills/linear-operations/SKILL.md +315 -0
- package/claude-skills/linear-operations/evals/evals.json +46 -0
- package/dist/commands/attachments.d.ts +2 -0
- package/dist/commands/attachments.js +57 -0
- package/dist/commands/batch.d.ts +2 -0
- package/dist/commands/batch.js +309 -0
- package/dist/commands/comments.d.ts +2 -0
- package/dist/commands/comments.js +272 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +15 -0
- package/dist/commands/cycles.d.ts +2 -0
- package/dist/commands/cycles.js +63 -0
- package/dist/commands/documents.d.ts +2 -0
- package/dist/commands/documents.js +175 -0
- package/dist/commands/embeds.d.ts +2 -0
- package/dist/commands/embeds.js +65 -0
- package/dist/commands/gdoc.d.ts +2 -0
- package/dist/commands/gdoc.js +37 -0
- package/dist/commands/graphql.d.ts +2 -0
- package/dist/commands/graphql.js +70 -0
- package/dist/commands/init/aliases.d.ts +109 -0
- package/dist/commands/init/aliases.js +569 -0
- package/dist/commands/init/defaults.d.ts +25 -0
- package/dist/commands/init/defaults.js +112 -0
- package/dist/commands/init/index.d.ts +18 -0
- package/dist/commands/init/index.js +182 -0
- package/dist/commands/init/shared.d.ts +88 -0
- package/dist/commands/init/shared.js +164 -0
- package/dist/commands/init/token.d.ts +50 -0
- package/dist/commands/init/token.js +141 -0
- package/dist/commands/init/workspace.d.ts +20 -0
- package/dist/commands/init/workspace.js +80 -0
- package/dist/commands/issue-id.d.ts +28 -0
- package/dist/commands/issue-id.js +81 -0
- package/dist/commands/issues.d.ts +2 -0
- package/dist/commands/issues.js +1145 -0
- package/dist/commands/labels.d.ts +2 -0
- package/dist/commands/labels.js +100 -0
- package/dist/commands/project-milestones.d.ts +2 -0
- package/dist/commands/project-milestones.js +143 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +336 -0
- package/dist/commands/read-shortcut.d.ts +6 -0
- package/dist/commands/read-shortcut.js +69 -0
- package/dist/commands/releases.d.ts +2 -0
- package/dist/commands/releases.js +142 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +171 -0
- package/dist/commands/teams.d.ts +2 -0
- package/dist/commands/teams.js +19 -0
- package/dist/commands/templates.d.ts +2 -0
- package/dist/commands/templates.js +58 -0
- package/dist/commands/users.d.ts +2 -0
- package/dist/commands/users.js +17 -0
- package/dist/config/config.d.ts +43 -0
- package/dist/config/config.js +81 -0
- package/dist/config/issue-validation.d.ts +39 -0
- package/dist/config/issue-validation.js +264 -0
- package/dist/config/paths.d.ts +20 -0
- package/dist/config/paths.js +22 -0
- package/dist/config/resolver.d.ts +25 -0
- package/dist/config/resolver.js +183 -0
- package/dist/config/status-defaults.d.ts +13 -0
- package/dist/config/status-defaults.js +20 -0
- package/dist/config/term-enforcer.d.ts +31 -0
- package/dist/config/term-enforcer.js +69 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +76 -0
- package/dist/queries/attachments.d.ts +3 -0
- package/dist/queries/attachments.js +35 -0
- package/dist/queries/comments.d.ts +3 -0
- package/dist/queries/comments.js +64 -0
- package/dist/queries/common.d.ts +2 -0
- package/dist/queries/common.js +109 -0
- package/dist/queries/cycles.d.ts +2 -0
- package/dist/queries/cycles.js +40 -0
- package/dist/queries/documents.d.ts +5 -0
- package/dist/queries/documents.js +67 -0
- package/dist/queries/introspect.d.ts +2 -0
- package/dist/queries/introspect.js +24 -0
- package/dist/queries/issues.d.ts +23 -0
- package/dist/queries/issues.js +380 -0
- package/dist/queries/labels.d.ts +4 -0
- package/dist/queries/labels.js +53 -0
- package/dist/queries/project-milestones.d.ts +6 -0
- package/dist/queries/project-milestones.js +125 -0
- package/dist/queries/projects.d.ts +7 -0
- package/dist/queries/projects.js +104 -0
- package/dist/queries/releases.d.ts +4 -0
- package/dist/queries/releases.js +85 -0
- package/dist/queries/search.d.ts +1 -0
- package/dist/queries/search.js +35 -0
- package/dist/queries/templates.d.ts +2 -0
- package/dist/queries/templates.js +30 -0
- package/dist/types/linear.d.ts +217 -0
- package/dist/types/linear.js +6 -0
- package/dist/utils/auth.d.ts +4 -0
- package/dist/utils/auth.js +23 -0
- package/dist/utils/auto-link-references.d.ts +47 -0
- package/dist/utils/auto-link-references.js +188 -0
- package/dist/utils/date-format.d.ts +4 -0
- package/dist/utils/date-format.js +8 -0
- package/dist/utils/download-uploads.d.ts +7 -0
- package/dist/utils/download-uploads.js +88 -0
- package/dist/utils/embed-parser.d.ts +8 -0
- package/dist/utils/embed-parser.js +54 -0
- package/dist/utils/error-messages.d.ts +4 -0
- package/dist/utils/error-messages.js +17 -0
- package/dist/utils/file-service.d.ts +13 -0
- package/dist/utils/file-service.js +239 -0
- package/dist/utils/gdoc-parser.d.ts +45 -0
- package/dist/utils/gdoc-parser.js +107 -0
- package/dist/utils/graphql-attachments-service.d.ts +12 -0
- package/dist/utils/graphql-attachments-service.js +46 -0
- package/dist/utils/graphql-documents-service.d.ts +18 -0
- package/dist/utils/graphql-documents-service.js +97 -0
- package/dist/utils/graphql-issues-service.d.ts +49 -0
- package/dist/utils/graphql-issues-service.js +925 -0
- package/dist/utils/graphql-service.d.ts +8 -0
- package/dist/utils/graphql-service.js +35 -0
- package/dist/utils/identifier-parser.d.ts +7 -0
- package/dist/utils/identifier-parser.js +20 -0
- package/dist/utils/issue-reference-extractor.d.ts +18 -0
- package/dist/utils/issue-reference-extractor.js +95 -0
- package/dist/utils/issue-reference-wrapper.d.ts +12 -0
- package/dist/utils/issue-reference-wrapper.js +91 -0
- package/dist/utils/linear-service.d.ts +26 -0
- package/dist/utils/linear-service.js +442 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.js +8 -0
- package/dist/utils/markdown-prosemirror.d.ts +24 -0
- package/dist/utils/markdown-prosemirror.js +325 -0
- package/dist/utils/mention-resolver.d.ts +31 -0
- package/dist/utils/mention-resolver.js +234 -0
- package/dist/utils/output.d.ts +7 -0
- package/dist/utils/output.js +125 -0
- package/dist/utils/table-formatter.d.ts +4 -0
- package/dist/utils/table-formatter.js +249 -0
- package/dist/utils/usage.d.ts +2 -0
- package/dist/utils/usage.js +24 -0
- package/dist/utils/uuid.d.ts +2 -0
- package/dist/utils/uuid.js +8 -0
- package/dist/utils/validate-references.d.ts +10 -0
- package/dist/utils/validate-references.js +33 -0
- package/dist/utils/validators.d.ts +7 -0
- package/dist/utils/validators.js +71 -0
- package/dist/utils/workspace-url.d.ts +4 -0
- package/dist/utils/workspace-url.js +44 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Enrich Layer
|
|
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
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="public/logo-256.png" alt="el-linear logo" width="160" height="160" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">el-linear</h1>
|
|
6
|
+
|
|
7
|
+
A pragmatic CLI for [Linear.app](https://linear.app) — deterministic team /
|
|
8
|
+
label / member resolution, structured issue validation, configurable term
|
|
9
|
+
enforcement, and a GraphQL escape hatch for everything that isn't a
|
|
10
|
+
first-class command.
|
|
11
|
+
|
|
12
|
+
> **Note on naming.** This package was briefly published as
|
|
13
|
+
> `@enrichlayer/linctl` with binary `linctl`, but reverted to
|
|
14
|
+
> `@enrichlayer/el-linear` (binary `el-linear`) because of an npm name
|
|
15
|
+
> collision with [dorkitude/linctl](https://github.com/dorkitude/linctl). See
|
|
16
|
+
> [CHANGELOG.md](./CHANGELOG.md) for the migration recipe.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pnpm add -g @enrichlayer/el-linear
|
|
22
|
+
# or
|
|
23
|
+
npm install -g @enrichlayer/el-linear
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Requires Node.js ≥ 22.
|
|
27
|
+
|
|
28
|
+
## Quickstart
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Run the interactive setup wizard. Only the API token is required;
|
|
32
|
+
# every other step is skippable and revisitable later.
|
|
33
|
+
el-linear init
|
|
34
|
+
|
|
35
|
+
# Sanity-check
|
|
36
|
+
el-linear teams list
|
|
37
|
+
|
|
38
|
+
# Create your first issue
|
|
39
|
+
el-linear issues create "Investigate flaky deploy" \
|
|
40
|
+
--team ENG --assignee alice --project "Reliability" \
|
|
41
|
+
--description "..."
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`el-linear init` walks you through the API token, default team, member
|
|
45
|
+
aliases, and other defaults. Each step is also a stand-alone sub-command
|
|
46
|
+
(`el-linear init token`, `el-linear init aliases --import users.csv`, etc.) so
|
|
47
|
+
you can revisit individual sections later. Skip is the default at every
|
|
48
|
+
prompt — running the wizard twice with no input is a no-op.
|
|
49
|
+
|
|
50
|
+
If you'd rather skip the wizard entirely, the configuration schema is
|
|
51
|
+
fully documented in [docs/configuration.md](./docs/configuration.md) so
|
|
52
|
+
any LLM or script can write `~/.config/el-linear/config.json` directly.
|
|
53
|
+
|
|
54
|
+
Output is JSON by default. Pipe through `jq` for ad-hoc queries, or use
|
|
55
|
+
the built-in `--jq` / `--fields` / `--raw` flags.
|
|
56
|
+
|
|
57
|
+
## Why el-linear
|
|
58
|
+
|
|
59
|
+
The Linear API + SDK are excellent. el-linear adds the layer above them that
|
|
60
|
+
every team ends up writing themselves:
|
|
61
|
+
|
|
62
|
+
| Concern | What el-linear gives you |
|
|
63
|
+
|---------|----------------------|
|
|
64
|
+
| **Name resolution** | Map team keys, member aliases, and label names to UUIDs from one config file. No fuzzy matching, no API roundtrips per call. |
|
|
65
|
+
| **Issue hygiene** | Required labels, required assignee, required project, type-label-to-verb conventions — all configurable. Warn or hard-fail. |
|
|
66
|
+
| **Term enforcement** | Catch misspellings of brand and project names in issue titles and descriptions ("EnrichLayer" → "Enrich Layer"). |
|
|
67
|
+
| **Status defaults** | "No project? → Triage. Has assignee+project? → Todo." Per-workspace, configurable. |
|
|
68
|
+
| **Auto-link & relate** | When you write `EMW-258` in a description, el-linear wraps it as a markdown link **and** creates the corresponding sidebar relation. Prose like "blocked by EMW-258" infers the relation type. |
|
|
69
|
+
| **`--claude` flag** | Tag issues delegated to [Claude Code](https://claude.ai/code) for autonomous work. The label is plain config; the flag is muscle memory. |
|
|
70
|
+
| **GraphQL escape hatch** | Anything not covered by built-in commands: `el-linear graphql '{ viewer { id } }'`. Schema introspection included. |
|
|
71
|
+
| **Bundled Claude skill** | The published tarball includes a `claude-skills/linear-operations/` directory you can symlink into your project's `.claude/skills/`. |
|
|
72
|
+
|
|
73
|
+
## Authentication
|
|
74
|
+
|
|
75
|
+
The API token is resolved in this order:
|
|
76
|
+
|
|
77
|
+
1. `--api-token <token>` flag.
|
|
78
|
+
2. `LINEAR_API_TOKEN` environment variable.
|
|
79
|
+
3. `~/.config/el-linear/token` file (recommended for human use).
|
|
80
|
+
4. `~/.linear_api_token` file (legacy, still honored).
|
|
81
|
+
|
|
82
|
+
el-linear never logs the token.
|
|
83
|
+
|
|
84
|
+
## Configuration
|
|
85
|
+
|
|
86
|
+
el-linear reads `~/.config/el-linear/config.json` on startup. All keys are
|
|
87
|
+
optional; defaults work for casual use.
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"defaultTeam": "ENG",
|
|
92
|
+
"defaultLabels": ["claude"],
|
|
93
|
+
"members": {
|
|
94
|
+
"aliases": { "alice": "Alice Anderson" },
|
|
95
|
+
"uuids": { "Alice Anderson": "<uuid-from-linear>" }
|
|
96
|
+
},
|
|
97
|
+
"teams": { "ENG": "<uuid-from-linear>" },
|
|
98
|
+
"labels": {
|
|
99
|
+
"workspace": { "claude": "<uuid-from-linear>" },
|
|
100
|
+
"teams": { "ENG": { "feature": "<uuid-from-linear>" } }
|
|
101
|
+
},
|
|
102
|
+
"statusDefaults": {
|
|
103
|
+
"noProject": "Triage",
|
|
104
|
+
"withAssigneeAndProject": "Todo"
|
|
105
|
+
},
|
|
106
|
+
"terms": [
|
|
107
|
+
{ "canonical": "Enrich Layer", "reject": ["EnrichLayer", "enrichlayer"] }
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
A full reference with every key documented lives in [config.example.json](./config.example.json).
|
|
113
|
+
|
|
114
|
+
UUIDs come from the Linear UI (URL bars, settings pages) or via el-linear
|
|
115
|
+
itself: `el-linear teams list --raw | jq '.[] | {key, id}'`, etc.
|
|
116
|
+
|
|
117
|
+
## Term enforcement (with brand-promotion examples)
|
|
118
|
+
|
|
119
|
+
The `terms` rules let you keep a list of canonical names and the misspellings
|
|
120
|
+
to reject. el-linear warns (or in `--strict` mode, throws) when an issue title
|
|
121
|
+
or description contains a rejected form.
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"terms": [
|
|
126
|
+
{ "canonical": "Enrich Layer", "reject": ["EnrichLayer", "enrichlayer", "Enrichlayer"] },
|
|
127
|
+
{ "canonical": "Linear", "reject": ["linear.app", "Linear App"] },
|
|
128
|
+
{ "canonical": "GitHub", "reject": ["Github", "GitHUB"] }
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
$ el-linear issues create "Add EnrichLayer auth flow" --team ENG --description "..." --strict
|
|
135
|
+
Term enforcement failed:
|
|
136
|
+
- Found "EnrichLayer" — use "Enrich Layer" instead (1 occurrence)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
URLs and file paths are exempt — `enrichlayer.com` and `path/to/enrichlayer`
|
|
140
|
+
are allowed even though `enrichlayer` is rejected.
|
|
141
|
+
|
|
142
|
+
If you don't define any rules, term enforcement is a no-op.
|
|
143
|
+
|
|
144
|
+
## The `--claude` delegation pattern
|
|
145
|
+
|
|
146
|
+
`el-linear issues create` accepts `--claude`, which applies the workspace label
|
|
147
|
+
configured at `config.labels.workspace.claude`. The label is the contract:
|
|
148
|
+
it tells [Claude Code](https://claude.ai/code) "this issue is delegated to
|
|
149
|
+
you for autonomous execution."
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
el-linear issues create "Migrate auth middleware to new session store" \
|
|
153
|
+
--team ENG --assignee alice --project "Auth Refactor" \
|
|
154
|
+
--description "..." --claude
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Claude finds delegated work with `el-linear issues search "claude" --status "Todo"`.
|
|
158
|
+
|
|
159
|
+
## Commands at a glance
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
el-linear usage # full reference for all commands
|
|
163
|
+
el-linear <command> --help # detailed help for one command
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
| Group | Common commands |
|
|
167
|
+
|-------|-----------------|
|
|
168
|
+
| Issues | `issues {list, search, create, read, update, delete, history, related, link-references}` |
|
|
169
|
+
| Comments | `comments {list, create, update}` |
|
|
170
|
+
| Labels | `labels {list, create, retire, restore}` |
|
|
171
|
+
| Projects | `projects {list, add-team, remove-team}` |
|
|
172
|
+
| Cycles | `cycles {list, read}` |
|
|
173
|
+
| Documents | `documents {list, read, create, update, delete}` |
|
|
174
|
+
| Releases | `releases {list, read, create, pipelines}` |
|
|
175
|
+
| Files | `embeds {upload, download}`, `attachments {list, create, delete}` |
|
|
176
|
+
| Search | `search <query>` (semantic, cross-resource) |
|
|
177
|
+
| Escape hatch | `graphql [query]` (with `--introspect`) |
|
|
178
|
+
| Config | `config show`, `users list`, `teams list`, `templates list` |
|
|
179
|
+
|
|
180
|
+
All `list` subcommands support `-l, --limit <n>`. All commands accept the
|
|
181
|
+
top-level filters: `--raw`, `--jq <expr>`, `--fields <list>`.
|
|
182
|
+
|
|
183
|
+
## Use with Claude Code
|
|
184
|
+
|
|
185
|
+
el-linear ships a Claude Code skill at `claude-skills/linear-operations/SKILL.md`.
|
|
186
|
+
After installing the package, symlink it into your project:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
PKG=$(npm root -g)/@enrichlayer/el-linear
|
|
190
|
+
ln -s "$PKG/claude-skills/linear-operations" .claude/skills/linear-operations
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The skill teaches Claude Code el-linear's syntax, the duplicate/related issue
|
|
194
|
+
check, the label taxonomy, the auto-link flow, and the `--claude` delegation
|
|
195
|
+
pattern.
|
|
196
|
+
|
|
197
|
+
## Development
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
git clone https://github.com/enrichlayer/el-linear.git
|
|
201
|
+
cd el-linear
|
|
202
|
+
pnpm install
|
|
203
|
+
|
|
204
|
+
pnpm test # vitest
|
|
205
|
+
pnpm exec tsc --noEmit # typecheck
|
|
206
|
+
pnpm exec biome check src/
|
|
207
|
+
pnpm run build
|
|
208
|
+
node dist/main.js --version
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full guide.
|
|
212
|
+
|
|
213
|
+
## Built by
|
|
214
|
+
|
|
215
|
+
[Enrich Layer](https://enrichlayer.com) — data enrichment APIs.
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
[MIT](./LICENSE).
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: linear-operations
|
|
3
|
+
description: "MUST be invoked before any el-linear CLI call. Covers el-linear syntax, label taxonomy, duplicate checking, issue creation conventions, and project management. Triggers on: \"create issue\", \"update issue\", \"search issues\", \"Linear\", \"el-linear\", \"label\", \"assign\". NOT for branch creation or full development workflows."
|
|
4
|
+
allowed-tools: Bash(el-linear:*), Bash(which:*), AskUserQuestion, Read
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Linear Operations Skill
|
|
8
|
+
|
|
9
|
+
All Linear operations should go through the **`el-linear` CLI**. For command syntax, run `el-linear usage` (all commands) or `el-linear <command> --help`.
|
|
10
|
+
|
|
11
|
+
This skill covers **mandatory processes and non-obvious rules** — everything the CLI help doesn't tell you.
|
|
12
|
+
|
|
13
|
+
> **Team-specific overrides.** Many teams keep their own issue-creation guide, label taxonomy, or member alias map. If your project has a `CLAUDE.md` or sibling skill that supplements this one, treat its rules as authoritative on top of these defaults.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Intent-Driven Issue Writing
|
|
18
|
+
|
|
19
|
+
Every issue should communicate **why** the work matters and **what** success looks like. This creates a track record of reasoning and gives the assignee room to find better solutions.
|
|
20
|
+
|
|
21
|
+
### Principles
|
|
22
|
+
|
|
23
|
+
1. **Lead with intent.** Start with why this work matters. The "Why we need this" section should explain real motivation (business problem, user pain, strategic context) — not restate the title.
|
|
24
|
+
2. **Describe outcomes.** "Done when" criteria describe what success looks like. The assignee may find a more elegant path than the one you'd prescribe.
|
|
25
|
+
3. **Respect expertise.** Provide context the assignee might lack (what a tool is, links to docs, business reasoning). Specific instructions are fine when you have relevant domain knowledge — but always pair them with the intent so the assignee understands the goal behind them.
|
|
26
|
+
|
|
27
|
+
### Example
|
|
28
|
+
|
|
29
|
+
```markdown
|
|
30
|
+
## Set up a self-hosted CRM
|
|
31
|
+
|
|
32
|
+
The team needs a CRM to replace fragmented contact tracking
|
|
33
|
+
(spreadsheets + Linear + memory). Should be production-ready:
|
|
34
|
+
reliable, backed up, accessible.
|
|
35
|
+
|
|
36
|
+
## Why we need this
|
|
37
|
+
As we scale outbound, we need customer relationships, pipeline,
|
|
38
|
+
and outreach tracked in one place.
|
|
39
|
+
|
|
40
|
+
## Done when
|
|
41
|
+
- Team can access the CRM UI and create/edit contacts.
|
|
42
|
+
- Data survives a server restart.
|
|
43
|
+
- Accessible via a subdomain with TLS.
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Duplicate & Related Issues Check (MANDATORY)
|
|
49
|
+
|
|
50
|
+
**Search before creating. No exceptions.**
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
el-linear issues search "keywords from proposed title" 2>&1
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
1. Extract 2–3 key terms from the proposed title (skip generic words).
|
|
57
|
+
2. Review results — classify each match:
|
|
58
|
+
- Same component + same problem = **duplicate** → comment on existing issue, don't create.
|
|
59
|
+
- Same component + different problem = **related** → create new issue with `--related-to`.
|
|
60
|
+
- Same domain but different component = **related** → create new issue with `--related-to`.
|
|
61
|
+
- Unrelated = proceed without linking.
|
|
62
|
+
3. If a potential duplicate is found: show the user the existing issue(s), ask whether to comment, mark duplicate, or proceed.
|
|
63
|
+
4. If related issues are found, **always create the relation** when creating the new issue:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
el-linear issues create "Title" --team ENG --related-to "ENG-456,ENG-789" ... 2>&1
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Viewing existing relations
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
el-linear issues related ENG-123 2>&1
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Returns all relations (related, blocks, blockedBy, duplicate) with direction, state, and assignee. Use this before creating follow-up work to understand the context around an issue.
|
|
76
|
+
|
|
77
|
+
### Auto-linking issue references on create/update
|
|
78
|
+
|
|
79
|
+
`el-linear issues create`, `el-linear issues update`, `el-linear comments create`, and `el-linear comments update` run the same auto-linking flow on text being written:
|
|
80
|
+
|
|
81
|
+
1. **Wrap as markdown links.** Bare identifiers (`ENG-100`, `DESIGN-22`) are rewritten to `[ENG-100](https://linear.app/<workspace>/issue/ENG-100/)`. Skipped inside fenced code blocks, inline backticks, existing markdown links, angle-bracket autolinks, and bare URLs.
|
|
82
|
+
2. **Validate first.** Identifiers that don't resolve in the workspace (e.g. ISO codes like `ISO-1424`) are left as plain text — no link, no relation.
|
|
83
|
+
3. **Create sidebar relations** on the parent issue. Default is `related`. Prose keywords upgrade the type:
|
|
84
|
+
- `blocked by X` / `depends on X` / `waiting on X` → `blocks` (X→source)
|
|
85
|
+
- `blocks X` / `prerequisite for X` → `blocks` (source→X)
|
|
86
|
+
- `duplicates X` / `duplicate of X` → `duplicate` (source→X)
|
|
87
|
+
- `duplicated by X` → `duplicate` (X→source)
|
|
88
|
+
|
|
89
|
+
The output includes an `autoLinked` field with `linked`, `skipped`, and `failed` arrays.
|
|
90
|
+
|
|
91
|
+
- A reference is **skipped** when any relation already exists.
|
|
92
|
+
- A reference is **failed** when the identifier doesn't resolve.
|
|
93
|
+
- Pass `--no-auto-link` on `create`/`update` to opt out of both wrapping and sidebar relation creation.
|
|
94
|
+
|
|
95
|
+
To backfill an existing issue:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
el-linear issues link-references ENG-123 # description only
|
|
99
|
+
el-linear issues link-references ENG-123 --include-comments # description + comments
|
|
100
|
+
el-linear issues link-references ENG-123 --dry-run # preview
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Pre-Work Check (Existing Issues)
|
|
106
|
+
|
|
107
|
+
When starting work on an existing issue (`el-linear issues read ENG-123`):
|
|
108
|
+
|
|
109
|
+
- [ ] **Assignee set** — if missing, ask user and update: `el-linear issues update ENG-123 --assignee <name>`.
|
|
110
|
+
- [ ] **Project set** — if missing, ask user and update: `el-linear issues update ENG-123 --project "<name>"`.
|
|
111
|
+
- [ ] **Status appropriate** — move to "In Progress" or your team's equivalent.
|
|
112
|
+
|
|
113
|
+
Don't start implementation work on an unassigned issue. The assignee is the person accountable.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Pre-Creation Validation Checklist
|
|
118
|
+
|
|
119
|
+
Complete ALL items before creating any issue:
|
|
120
|
+
|
|
121
|
+
- [ ] **Duplicate & related check** — searched for existing issues, linked related ones (above).
|
|
122
|
+
- [ ] **Team** — ask user if unclear (`el-linear teams list`).
|
|
123
|
+
- [ ] **Assignee** — ask user if unclear (`el-linear users list --active`).
|
|
124
|
+
- [ ] **Project** — always ask user, never guess (`el-linear projects list`).
|
|
125
|
+
- [ ] **Labels** — exactly 1 type label + 1–2 domain labels (see Label Taxonomy below).
|
|
126
|
+
- [ ] **Title** — action verb matching the type label (see Title Verb Convention), sentence case, specific scope.
|
|
127
|
+
- [ ] **Description** — 2–4 sentences with context and intent, formatted with **bold** and `inline code`.
|
|
128
|
+
- [ ] **"Why we need this"** — genuine motivation, not a restatement of the title.
|
|
129
|
+
|
|
130
|
+
If any field is missing, **STOP and use AskUserQuestion**.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## The `--claude` Delegation Pattern
|
|
135
|
+
|
|
136
|
+
`el-linear issues create` accepts a `--claude` flag that applies the workspace-level "claude" label (configured at `config.labels.workspace.claude`). This is the canonical signal that an issue is **delegated to Claude Code** for autonomous execution.
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
el-linear issues create "Migrate auth middleware to new session store" \
|
|
140
|
+
--team ENG --assignee alice --project "Auth Refactor" \
|
|
141
|
+
--description "..." --claude 2>&1
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
When you see `--claude` in usage:
|
|
145
|
+
|
|
146
|
+
- **As a writer**: use it to mark issues you want Claude to pick up. The label is the contract; combine it with a clear "Done when" so Claude knows what success looks like.
|
|
147
|
+
- **As Claude**: if you find an issue with the `claude` label, you should treat it as in-scope work for autonomous progress. Search with `el-linear issues search "claude" --status "Todo"`.
|
|
148
|
+
- **As a reviewer**: an issue's `claude` label tells you to weigh whether the description has enough acceptance criteria, since the assignee can't ask follow-up questions in a back-and-forth.
|
|
149
|
+
|
|
150
|
+
The label is plain config — set it to anything you want, or skip it entirely. The flag is just sugar for `--labels claude`.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## User @Mentions
|
|
155
|
+
|
|
156
|
+
Reference team members by name in comments. el-linear resolves both explicit `@name` tokens and bare capitalized references to proper Linear mentions.
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
el-linear comments create ENG-123 --body "cc @alice — Bob can you review?" 2>&1
|
|
160
|
+
# Both "@alice" and "Bob" become Linear mentions.
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- **Explicit `@name`** — always resolved. Config alias → display name → name (partial, case-insensitive) → API fallback.
|
|
164
|
+
- **Bare capitalized names** — auto-converted by default when they match a configured member. Config-only — no API fallback — so false positives are bounded to your team. Skipped inside inline code, code blocks, and link text. Self-references are skipped.
|
|
165
|
+
- **Opt out** per-invocation with `--no-auto-mention`.
|
|
166
|
+
|
|
167
|
+
Works in comments only (not descriptions).
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## CLI Syntax Rules
|
|
172
|
+
|
|
173
|
+
Run `el-linear usage` for the full command reference. Non-obvious rules:
|
|
174
|
+
|
|
175
|
+
- **Always append `2>&1`** to capture errors.
|
|
176
|
+
- **Labels are comma-separated** — `--labels "feature,backend"` (not repeated flags).
|
|
177
|
+
- **Singular/plural interchangeable** — `issues`/`issue`, `labels`/`label`, etc.
|
|
178
|
+
- **Subcommand aliases** — `read`/`view`/`get`/`show`, `update`/`edit`/`set`.
|
|
179
|
+
- **`--jq` for GraphQL filtering** — never pipe through `jq` directly (zsh escaping breaks `!=`).
|
|
180
|
+
- **`--raw` flag** strips the `{ data, meta }` wrapper — emits just the array.
|
|
181
|
+
|
|
182
|
+
### Output format
|
|
183
|
+
|
|
184
|
+
| Command type | Shape |
|
|
185
|
+
|--------------|-------|
|
|
186
|
+
| List | `{ "data": [...], "meta": { "count": N } }` |
|
|
187
|
+
| Single resource | Flat object |
|
|
188
|
+
| Error | `{ "error": "message" }` |
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Error Handling
|
|
193
|
+
|
|
194
|
+
1. **Check for `"error"` key** before accessing fields like `identifier`.
|
|
195
|
+
2. **Do NOT retry** failed commands — report the error.
|
|
196
|
+
3. Common errors and fixes:
|
|
197
|
+
- "Label not found" → check `el-linear labels list --team X`.
|
|
198
|
+
- "Label is a group label" → use a child label, not the parent.
|
|
199
|
+
- "Project may not be associated with this issue's team" → fix with `el-linear projects add-team`.
|
|
200
|
+
- "User not found" → check `el-linear users list --active`.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Label Taxonomy
|
|
205
|
+
|
|
206
|
+
### Type Labels (Required: exactly 1)
|
|
207
|
+
|
|
208
|
+
The default el-linear validation expects one of these:
|
|
209
|
+
|
|
210
|
+
| Label | When |
|
|
211
|
+
|-------|------|
|
|
212
|
+
| `feature` | New functionality. |
|
|
213
|
+
| `bug` | Broken behavior. |
|
|
214
|
+
| `refactor` | Improving code without changing behavior. |
|
|
215
|
+
| `chore` | Maintenance, dependencies, tooling. |
|
|
216
|
+
| `spike` | Research or investigation. |
|
|
217
|
+
|
|
218
|
+
Customize the set per-config: `validation.typeLabels: ["feature", "bug", ...]`. Domain labels (`frontend`, `backend`, etc.) are not enforced — define your own.
|
|
219
|
+
|
|
220
|
+
### Title Verb Convention
|
|
221
|
+
|
|
222
|
+
Title must start with a verb that matches the type label:
|
|
223
|
+
|
|
224
|
+
- `bug` → Fix, Resolve, Patch, Handle, Address, Correct
|
|
225
|
+
- `feature` → Add, Build, Create, Implement, Enable, Ship, Launch, Design, Wire, Integrate
|
|
226
|
+
- `chore` → Update, Remove, Clean, Migrate, Deploy, Rotate, Configure, Document, Review, Publish, Standardize, Upgrade
|
|
227
|
+
- `spike` → Research, Investigate, Explore, Evaluate, Audit, Benchmark
|
|
228
|
+
- `refactor` → Refactor, Restructure, Extract, Decouple, Simplify
|
|
229
|
+
|
|
230
|
+
### Rules
|
|
231
|
+
|
|
232
|
+
- **Create missing labels liberally** — `el-linear labels create "my-label" --team ENG`.
|
|
233
|
+
- **`--claude` flag** adds the workspace-level "claude" label automatically.
|
|
234
|
+
- Ask the user to confirm labels if you're unsure about domain.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Term Enforcement (`config.terms`)
|
|
239
|
+
|
|
240
|
+
el-linear can flag misspellings of brand or project names in titles and descriptions. Configure rules in `~/.config/el-linear/config.json`:
|
|
241
|
+
|
|
242
|
+
```json
|
|
243
|
+
{
|
|
244
|
+
"terms": [
|
|
245
|
+
{ "canonical": "Enrich Layer", "reject": ["EnrichLayer", "enrichlayer"] },
|
|
246
|
+
{ "canonical": "Linear", "reject": ["linear.app", "Linear App"] },
|
|
247
|
+
{ "canonical": "GitHub", "reject": ["Github", "github"] }
|
|
248
|
+
]
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
When el-linear finds a rejected token in an issue title or description, it warns (or in `--strict` mode, throws). The check tolerates URLs and file paths — `enrichlayer.com` is allowed even though `enrichlayer` is rejected.
|
|
253
|
+
|
|
254
|
+
If you don't configure any rules, term enforcement is a no-op.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Project Management Gotchas
|
|
259
|
+
|
|
260
|
+
### Content vs Description
|
|
261
|
+
|
|
262
|
+
Linear projects have two text fields — **both must be populated**:
|
|
263
|
+
|
|
264
|
+
- `description` — short summary (max 255 chars), shown in lists.
|
|
265
|
+
- `content` — full markdown body, shown in main panel.
|
|
266
|
+
|
|
267
|
+
If you only set one, the other shows blank.
|
|
268
|
+
|
|
269
|
+
### Cross-Team Projects
|
|
270
|
+
|
|
271
|
+
Creating an issue on team X with a project from team Y → "Project not in same team" error. Fix:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
el-linear projects add-team "Project Name" ENG 2>&1
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**Never use raw `projectUpdate` with `teamIds`** — it replaces the entire team list. Always use `projects add-team` / `remove-team`.
|
|
278
|
+
|
|
279
|
+
### Discovery Before Creation
|
|
280
|
+
|
|
281
|
+
Always check if a project exists before creating: `el-linear projects list --limit 50`.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Git/GitLab/GitHub Integration
|
|
286
|
+
|
|
287
|
+
If your Linear workspace is connected to a code host, status transitions happen automatically:
|
|
288
|
+
|
|
289
|
+
- Creating a PR/MR → linked issue moves to "In Review".
|
|
290
|
+
- Merging a PR/MR → linked issue moves to "Done".
|
|
291
|
+
|
|
292
|
+
No manual status updates needed after PR/MR events.
|
|
293
|
+
|
|
294
|
+
### Intermediate Deliverable Rule
|
|
295
|
+
|
|
296
|
+
**Any PR/MR whose branch contains an issue ID will auto-close that issue on merge.** This is dangerous for multi-phase issues where a PR delivers only part of the work.
|
|
297
|
+
|
|
298
|
+
**Rule:** if the PR doesn't complete ALL acceptance criteria of the parent issue, create a sub-issue and branch from that. The parent stays open.
|
|
299
|
+
|
|
300
|
+
```text
|
|
301
|
+
Parent: ENG-100 "Review API design" ← stays open
|
|
302
|
+
└─ Sub: ENG-101 "Write API design doc" ← branch: feature/ENG-101-write-api-design-doc
|
|
303
|
+
PR merges → only ENG-101 closes
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Checklist Progress Tracking
|
|
309
|
+
|
|
310
|
+
When working on an issue with a checklist, check off items as you complete them.
|
|
311
|
+
|
|
312
|
+
1. Read the issue: `el-linear issues read ENG-123`.
|
|
313
|
+
2. Update the description with checked items: `el-linear issues update ENG-123 --description "..."`.
|
|
314
|
+
3. Preserve the rest of the description — only change `- [ ]` to `- [x]`.
|
|
315
|
+
4. If all items are done, update the status accordingly.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "linear-operations",
|
|
3
|
+
"evals": [
|
|
4
|
+
{
|
|
5
|
+
"id": 1,
|
|
6
|
+
"prompt": "Create a Linear issue for fixing the login bug on the staging environment",
|
|
7
|
+
"expected_output": "The skill searches for duplicate issues first, selects appropriate labels from the taxonomy, creates an issue with a description containing 'Why we need this' section, and uses el-linear create command",
|
|
8
|
+
"expectations": [
|
|
9
|
+
"Searches for existing issues to avoid duplicates before creating",
|
|
10
|
+
"Selects labels from the known label taxonomy (e.g., bug)",
|
|
11
|
+
"Description includes a 'Why we need this' section",
|
|
12
|
+
"Uses el-linear CLI to create the issue, not raw curl"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": 2,
|
|
17
|
+
"prompt": "Search Linear for auth-related issues",
|
|
18
|
+
"expected_output": "The skill uses el-linear search to find issues matching auth keywords",
|
|
19
|
+
"expectations": [
|
|
20
|
+
"Uses el-linear search command with appropriate query",
|
|
21
|
+
"Does not attempt to use curl against api.linear.app",
|
|
22
|
+
"Returns formatted results with issue IDs and titles"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": 3,
|
|
27
|
+
"prompt": "Update DEV-123 to In Progress",
|
|
28
|
+
"expected_output": "The skill uses el-linear update to change the issue status",
|
|
29
|
+
"expectations": [
|
|
30
|
+
"Uses el-linear update command with the issue identifier DEV-123",
|
|
31
|
+
"Sets the status to In Progress",
|
|
32
|
+
"Does not create a new issue"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": 4,
|
|
37
|
+
"prompt": "Create a git branch for DEV-456",
|
|
38
|
+
"expected_output": "This should NOT trigger linear-operations. Branch creation from Linear issues is handled by git-branch-from-linear skill.",
|
|
39
|
+
"expectations": [
|
|
40
|
+
"Does NOT trigger linear-operations skill",
|
|
41
|
+
"Routes to git-branch-from-linear instead",
|
|
42
|
+
"No el-linear create or update commands are issued"
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { getApiToken } from "../utils/auth.js";
|
|
2
|
+
import { FileService } from "../utils/file-service.js";
|
|
3
|
+
import { createGraphQLAttachmentsService } from "../utils/graphql-attachments-service.js";
|
|
4
|
+
import { createLinearService } from "../utils/linear-service.js";
|
|
5
|
+
import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
|
|
6
|
+
export function setupAttachmentsCommands(program) {
|
|
7
|
+
const attachments = program
|
|
8
|
+
.command("attachments")
|
|
9
|
+
.description("Manage file attachments on issues.");
|
|
10
|
+
attachments.action(() => attachments.help());
|
|
11
|
+
attachments
|
|
12
|
+
.command("create <issueId>")
|
|
13
|
+
.description("Upload a file and attach it to an issue.")
|
|
14
|
+
.requiredOption("--file <path>", "path to file to upload")
|
|
15
|
+
.option("--title <title>", "attachment title (defaults to filename)")
|
|
16
|
+
.action(handleAsyncCommand(async (issueId, options, command) => {
|
|
17
|
+
const rootOpts = command.parent.parent.opts();
|
|
18
|
+
const apiToken = getApiToken(rootOpts);
|
|
19
|
+
const linearService = createLinearService(rootOpts);
|
|
20
|
+
const resolvedIssueId = await linearService.resolveIssueId(issueId);
|
|
21
|
+
const fileService = new FileService(apiToken);
|
|
22
|
+
const uploadResult = await fileService.uploadFile(options.file);
|
|
23
|
+
if (!uploadResult.success) {
|
|
24
|
+
throw new Error(uploadResult.error);
|
|
25
|
+
}
|
|
26
|
+
const attachmentsService = createGraphQLAttachmentsService(rootOpts);
|
|
27
|
+
const attachment = await attachmentsService.createAttachment({
|
|
28
|
+
issueId: resolvedIssueId,
|
|
29
|
+
url: uploadResult.assetUrl,
|
|
30
|
+
title: options.title || uploadResult.filename,
|
|
31
|
+
});
|
|
32
|
+
outputSuccess(attachment);
|
|
33
|
+
}));
|
|
34
|
+
attachments
|
|
35
|
+
.command("list <issueId>")
|
|
36
|
+
.description("List attachments on an issue.")
|
|
37
|
+
.option("-l, --limit <number>", "maximum number of attachments", "50")
|
|
38
|
+
.action(handleAsyncCommand(async (issueId, options, command) => {
|
|
39
|
+
const rootOpts = command.parent.parent.opts();
|
|
40
|
+
const linearService = createLinearService(rootOpts);
|
|
41
|
+
const resolvedIssueId = await linearService.resolveIssueId(issueId);
|
|
42
|
+
const attachmentsService = createGraphQLAttachmentsService(rootOpts);
|
|
43
|
+
const allAttachments = await attachmentsService.listAttachments(resolvedIssueId);
|
|
44
|
+
const limit = Number.parseInt(options.limit, 10);
|
|
45
|
+
const data = allAttachments.slice(0, limit);
|
|
46
|
+
outputSuccess({ data, meta: { count: data.length } });
|
|
47
|
+
}));
|
|
48
|
+
attachments
|
|
49
|
+
.command("delete <attachmentId>")
|
|
50
|
+
.description("Delete an attachment.")
|
|
51
|
+
.action(handleAsyncCommand(async (attachmentId, _options, command) => {
|
|
52
|
+
const rootOpts = command.parent.parent.opts();
|
|
53
|
+
const attachmentsService = createGraphQLAttachmentsService(rootOpts);
|
|
54
|
+
await attachmentsService.deleteAttachment(attachmentId);
|
|
55
|
+
outputSuccess({ success: true, message: "Attachment deleted" });
|
|
56
|
+
}));
|
|
57
|
+
}
|