@ebragas/linear-cli 0.9.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.
Files changed (3) hide show
  1. package/README.md +211 -0
  2. package/dist/cli.js +2321 -0
  3. package/package.json +54 -0
package/dist/cli.js ADDED
@@ -0,0 +1,2321 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_commander = require("commander");
28
+ var import_fs7 = require("fs");
29
+ var import_path4 = require("path");
30
+
31
+ // src/commands/auth.ts
32
+ var import_http = __toESM(require("http"));
33
+ var import_sdk = require("@linear/sdk");
34
+
35
+ // src/credentials.ts
36
+ var import_fs = require("fs");
37
+ var import_path = require("path");
38
+ var import_os = require("os");
39
+ var REQUIRED_FIELDS = [
40
+ "authMethod",
41
+ "clientId",
42
+ "clientSecret",
43
+ "accessToken",
44
+ "tokenExpiresAt",
45
+ "actorId",
46
+ "workspaceId",
47
+ "workspaceSlug"
48
+ ];
49
+ function getCredentialsDir(opts) {
50
+ const dir = opts?.credentialsDir ?? process.env.LINEAR_AGENT_CREDENTIALS_DIR ?? (0, import_path.join)((0, import_os.homedir)(), ".linear", "credentials");
51
+ return dir.startsWith("~") ? (0, import_path.join)((0, import_os.homedir)(), dir.slice(2)) : (0, import_path.resolve)(dir);
52
+ }
53
+ function credentialsPath(agentId, credentialsDir) {
54
+ return (0, import_path.join)(credentialsDir, `${agentId}.json`);
55
+ }
56
+ function readCredentials(agentId, credentialsDir) {
57
+ const path = credentialsPath(agentId, credentialsDir);
58
+ let raw;
59
+ try {
60
+ raw = (0, import_fs.readFileSync)(path, "utf-8");
61
+ } catch {
62
+ throw new Error(
63
+ `Credentials not found for agent "${agentId}" at ${path}. Run "linear auth setup" first.`
64
+ );
65
+ }
66
+ const data = JSON.parse(raw);
67
+ const missing = REQUIRED_FIELDS.filter(
68
+ (f) => data[f] === void 0 || data[f] === null
69
+ );
70
+ if (missing.length > 0) {
71
+ throw new Error(
72
+ `Credentials file missing required fields: ${missing.join(", ")}`
73
+ );
74
+ }
75
+ return data;
76
+ }
77
+ function writeCredentials(agentId, credentialsDir, data) {
78
+ (0, import_fs.mkdirSync)(credentialsDir, { recursive: true, mode: 448 });
79
+ const path = credentialsPath(agentId, credentialsDir);
80
+ (0, import_fs.writeFileSync)(path, JSON.stringify(data, null, 2) + "\n", {
81
+ mode: 384
82
+ });
83
+ }
84
+ function deleteCredentials(agentId, credentialsDir) {
85
+ const path = credentialsPath(agentId, credentialsDir);
86
+ try {
87
+ const { unlinkSync } = require("fs");
88
+ unlinkSync(path);
89
+ } catch {
90
+ }
91
+ }
92
+
93
+ // src/errors.ts
94
+ var CLIError = class extends Error {
95
+ constructor(message, exitCode, resolution) {
96
+ super(message);
97
+ this.exitCode = exitCode;
98
+ this.resolution = resolution;
99
+ this.name = this.constructor.name;
100
+ }
101
+ };
102
+ var RateLimitError = class extends CLIError {
103
+ constructor(message, resetAt) {
104
+ super(message, 1, "Wait for rate limit reset or reduce request frequency.");
105
+ this.resetAt = resetAt;
106
+ }
107
+ };
108
+ var AuthenticationError = class extends CLIError {
109
+ constructor(message) {
110
+ super(message, 2, 'Run "linear auth setup" to re-authenticate.');
111
+ }
112
+ };
113
+ var ForbiddenError = class extends CLIError {
114
+ constructor(message) {
115
+ super(
116
+ message,
117
+ 3,
118
+ "The agent may have lost access. Check team permissions in Linear."
119
+ );
120
+ }
121
+ };
122
+ var ValidationError = class extends CLIError {
123
+ constructor(message, validOptions) {
124
+ const resolution = validOptions?.length ? `Valid options:
125
+ ${validOptions.map((o) => ` - ${o}`).join("\n")}` : void 0;
126
+ super(message, 4, resolution);
127
+ this.validOptions = validOptions;
128
+ }
129
+ };
130
+ var NetworkError = class extends CLIError {
131
+ constructor(message) {
132
+ super(message, 5, "Check network connectivity and try again.");
133
+ }
134
+ };
135
+ var PartialSuccessError = class extends CLIError {
136
+ constructor(message, succeeded, failed) {
137
+ super(message, 6);
138
+ this.succeeded = succeeded;
139
+ this.failed = failed;
140
+ }
141
+ };
142
+ function classifyError(err) {
143
+ if (err instanceof CLIError) return err;
144
+ const message = err instanceof Error ? err.message : String(err);
145
+ const errObj = err;
146
+ const type = errObj?.type ?? errObj?.extensions?.code;
147
+ switch (type) {
148
+ case "RATELIMITED":
149
+ return new RateLimitError(message);
150
+ case "AUTHENTICATION_ERROR":
151
+ return new AuthenticationError(message);
152
+ case "FORBIDDEN":
153
+ return new ForbiddenError(message);
154
+ case "InvalidInputLinearError":
155
+ return new ValidationError(message);
156
+ default:
157
+ if (message.includes("ECONNREFUSED") || message.includes("ENOTFOUND") || message.includes("ETIMEDOUT") || message.includes("fetch failed")) {
158
+ return new NetworkError(message);
159
+ }
160
+ return new CLIError(message, 1);
161
+ }
162
+ }
163
+
164
+ // src/output.ts
165
+ function getFormat(formatFlag) {
166
+ if (formatFlag === "json" || formatFlag === "text") return formatFlag;
167
+ return process.stdout.isTTY ? "text" : "json";
168
+ }
169
+ function formatOutput(result, format) {
170
+ if (format === "json") {
171
+ return formatJson(result);
172
+ }
173
+ return formatText(result);
174
+ }
175
+ function formatJson(result) {
176
+ const { data, warnings } = result;
177
+ if (Array.isArray(data)) {
178
+ const obj = { results: data };
179
+ if (warnings?.length) obj.warnings = warnings;
180
+ return JSON.stringify(obj, null, 2);
181
+ }
182
+ if (warnings?.length) {
183
+ return JSON.stringify({ ...data, _warnings: warnings }, null, 2);
184
+ }
185
+ return JSON.stringify(data, null, 2);
186
+ }
187
+ function formatText(result) {
188
+ const { data, warnings } = result;
189
+ const lines = [];
190
+ if (Array.isArray(data)) {
191
+ for (const item of data) {
192
+ lines.push(formatRecord(item));
193
+ }
194
+ } else if (data && typeof data === "object") {
195
+ lines.push(formatRecord(data));
196
+ } else {
197
+ lines.push(String(data));
198
+ }
199
+ if (warnings?.length) {
200
+ lines.push("");
201
+ for (const w of warnings) {
202
+ lines.push(`Warning: ${w}`);
203
+ }
204
+ }
205
+ return lines.join("\n");
206
+ }
207
+ function formatRecord(item) {
208
+ if (!item || typeof item !== "object") return String(item);
209
+ const record = item;
210
+ return Object.entries(record).map(([key, value]) => {
211
+ if (value === null || value === void 0) return `${key}: -`;
212
+ if (typeof value === "object") return `${key}: ${JSON.stringify(value)}`;
213
+ return `${key}: ${value}`;
214
+ }).join("\n");
215
+ }
216
+ function printResult(result, format) {
217
+ console.log(formatOutput(result, format));
218
+ }
219
+
220
+ // src/commands/auth.ts
221
+ var TOKEN_URL = "https://api.linear.app/oauth/token";
222
+ var AUTHORIZE_URL = "https://linear.app/oauth/authorize";
223
+ var DEFAULT_SCOPES = "read,write,app:assignable,app:mentionable";
224
+ async function fetchToken(params) {
225
+ const response = await fetch(TOKEN_URL, {
226
+ method: "POST",
227
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
228
+ body: new URLSearchParams(params).toString()
229
+ });
230
+ if (!response.ok) {
231
+ const body = await response.text();
232
+ throw new AuthenticationError(
233
+ `Token request failed: ${response.status} ${response.statusText}
234
+ ${body}`
235
+ );
236
+ }
237
+ return await response.json();
238
+ }
239
+ async function fetchViewerAndOrg(accessToken) {
240
+ const client = new import_sdk.LinearClient({ accessToken });
241
+ const viewer = await client.viewer;
242
+ const org = await client.organization;
243
+ return {
244
+ actorId: viewer.id,
245
+ name: viewer.name ?? viewer.id,
246
+ workspaceId: org.id,
247
+ workspaceSlug: org.urlKey
248
+ };
249
+ }
250
+ async function setupClientCredentials(opts) {
251
+ const tokenData = await fetchToken({
252
+ grant_type: "client_credentials",
253
+ client_id: opts.clientId,
254
+ client_secret: opts.clientSecret,
255
+ scope: opts.scopes
256
+ });
257
+ const { actorId, name, workspaceId, workspaceSlug } = await fetchViewerAndOrg(tokenData.access_token);
258
+ const credentials = {
259
+ authMethod: "client_credentials",
260
+ clientId: opts.clientId,
261
+ clientSecret: opts.clientSecret,
262
+ accessToken: tokenData.access_token,
263
+ refreshToken: null,
264
+ tokenExpiresAt: new Date(
265
+ Date.now() + tokenData.expires_in * 1e3
266
+ ).toISOString(),
267
+ actorId,
268
+ workspaceId,
269
+ workspaceSlug
270
+ };
271
+ writeCredentials(opts.agent, opts.credentialsDir, credentials);
272
+ const format = getFormat(opts.format);
273
+ printResult(
274
+ {
275
+ data: {
276
+ status: "authenticated",
277
+ agent: opts.agent,
278
+ actorId,
279
+ name,
280
+ workspace: workspaceSlug,
281
+ expiresAt: credentials.tokenExpiresAt
282
+ }
283
+ },
284
+ format
285
+ );
286
+ }
287
+ async function setupOAuth(opts) {
288
+ const redirectUri = `http://localhost:${opts.port}/callback`;
289
+ const code = await new Promise((resolve3, reject) => {
290
+ const server = import_http.default.createServer((req, res) => {
291
+ const url = new URL(req.url ?? "/", `http://localhost:${opts.port}`);
292
+ if (url.pathname === "/callback") {
293
+ const code2 = url.searchParams.get("code");
294
+ const error = url.searchParams.get("error");
295
+ if (error) {
296
+ res.writeHead(400, { "Content-Type": "text/html" });
297
+ res.end(`<h1>Authorization failed</h1><p>${error}</p>`);
298
+ server.close();
299
+ reject(
300
+ new AuthenticationError(`OAuth authorization failed: ${error}`)
301
+ );
302
+ return;
303
+ }
304
+ if (code2) {
305
+ res.writeHead(200, { "Content-Type": "text/html" });
306
+ res.end(
307
+ "<h1>Authorization successful</h1><p>You can close this window.</p>"
308
+ );
309
+ server.close();
310
+ resolve3(code2);
311
+ return;
312
+ }
313
+ }
314
+ res.writeHead(404);
315
+ res.end("Not found");
316
+ });
317
+ server.listen(opts.port, () => {
318
+ const authUrl = new URL(AUTHORIZE_URL);
319
+ authUrl.searchParams.set("response_type", "code");
320
+ authUrl.searchParams.set("client_id", opts.clientId);
321
+ authUrl.searchParams.set("redirect_uri", redirectUri);
322
+ authUrl.searchParams.set("scope", opts.scopes);
323
+ authUrl.searchParams.set("actor", "app");
324
+ console.log(`
325
+ Open this URL in your browser:
326
+ ${authUrl.toString()}
327
+ `);
328
+ console.log(`Waiting for callback on port ${opts.port}...`);
329
+ });
330
+ server.on("error", (err) => {
331
+ reject(
332
+ new AuthenticationError(
333
+ `Failed to start callback server: ${err.message}`
334
+ )
335
+ );
336
+ });
337
+ setTimeout(() => {
338
+ server.close();
339
+ reject(new AuthenticationError("OAuth callback timed out after 5 minutes"));
340
+ }, 5 * 60 * 1e3);
341
+ });
342
+ const tokenData = await fetchToken({
343
+ grant_type: "authorization_code",
344
+ code,
345
+ redirect_uri: redirectUri,
346
+ client_id: opts.clientId,
347
+ client_secret: opts.clientSecret
348
+ });
349
+ const { actorId, name, workspaceId, workspaceSlug } = await fetchViewerAndOrg(tokenData.access_token);
350
+ const credentials = {
351
+ authMethod: "oauth",
352
+ clientId: opts.clientId,
353
+ clientSecret: opts.clientSecret,
354
+ accessToken: tokenData.access_token,
355
+ refreshToken: tokenData.refresh_token ?? null,
356
+ tokenExpiresAt: new Date(
357
+ Date.now() + tokenData.expires_in * 1e3
358
+ ).toISOString(),
359
+ actorId,
360
+ workspaceId,
361
+ workspaceSlug
362
+ };
363
+ writeCredentials(opts.agent, opts.credentialsDir, credentials);
364
+ const format = getFormat(opts.format);
365
+ printResult(
366
+ {
367
+ data: {
368
+ status: "authenticated",
369
+ agent: opts.agent,
370
+ actorId,
371
+ name,
372
+ workspace: workspaceSlug,
373
+ method: "oauth",
374
+ expiresAt: credentials.tokenExpiresAt
375
+ }
376
+ },
377
+ format
378
+ );
379
+ }
380
+ function registerAuthCommands(program2) {
381
+ const auth = program2.command("auth").description("Manage authentication and API tokens");
382
+ auth.command("setup").description("Authenticate an agent with Linear").requiredOption("--client-id <id>", "OAuth application client ID").requiredOption("--client-secret <secret>", "OAuth application client secret").option("--client-credentials", "Use client credentials grant (default)", true).option("--oauth", "Use OAuth authorization code flow").option("--port <port>", "Local callback server port (OAuth only)", "9876").option("--scopes <scopes>", "OAuth scopes", DEFAULT_SCOPES).action(async (opts, cmd) => {
383
+ const globalOpts = cmd.optsWithGlobals();
384
+ const agent = globalOpts.agent;
385
+ if (!agent) {
386
+ console.error(
387
+ "Error: --agent is required (or set LINEAR_AGENT_ID env var)"
388
+ );
389
+ process.exit(4);
390
+ }
391
+ const credentialsDir = getCredentialsDir(globalOpts);
392
+ const format = globalOpts.format;
393
+ if (opts.oauth) {
394
+ await setupOAuth({
395
+ agent,
396
+ clientId: opts.clientId,
397
+ clientSecret: opts.clientSecret,
398
+ scopes: opts.scopes,
399
+ port: parseInt(opts.port, 10),
400
+ credentialsDir,
401
+ format
402
+ });
403
+ } else {
404
+ await setupClientCredentials({
405
+ agent,
406
+ clientId: opts.clientId,
407
+ clientSecret: opts.clientSecret,
408
+ scopes: opts.scopes,
409
+ credentialsDir,
410
+ format
411
+ });
412
+ }
413
+ });
414
+ auth.command("whoami").description("Verify token and print agent identity").action(async (_opts, cmd) => {
415
+ const globalOpts = cmd.optsWithGlobals();
416
+ const agent = globalOpts.agent;
417
+ if (!agent) {
418
+ console.error(
419
+ "Error: --agent is required (or set LINEAR_AGENT_ID env var)"
420
+ );
421
+ process.exit(4);
422
+ }
423
+ const credentialsDir = getCredentialsDir(globalOpts);
424
+ const credentials = readCredentials(agent, credentialsDir);
425
+ const client = new import_sdk.LinearClient({
426
+ accessToken: credentials.accessToken
427
+ });
428
+ const viewer = await client.viewer;
429
+ const org = await client.organization;
430
+ const format = getFormat(globalOpts.format);
431
+ printResult(
432
+ {
433
+ data: {
434
+ agent,
435
+ actorId: credentials.actorId,
436
+ name: viewer.name ?? viewer.id,
437
+ email: viewer.email,
438
+ workspace: org.urlKey,
439
+ workspaceId: org.id,
440
+ authMethod: credentials.authMethod,
441
+ tokenExpiresAt: credentials.tokenExpiresAt
442
+ }
443
+ },
444
+ format
445
+ );
446
+ });
447
+ auth.command("refresh").description("Request a new token").action(async (_opts, cmd) => {
448
+ const globalOpts = cmd.optsWithGlobals();
449
+ const agent = globalOpts.agent;
450
+ if (!agent) {
451
+ console.error(
452
+ "Error: --agent is required (or set LINEAR_AGENT_ID env var)"
453
+ );
454
+ process.exit(4);
455
+ }
456
+ const credentialsDir = getCredentialsDir(globalOpts);
457
+ const credentials = readCredentials(agent, credentialsDir);
458
+ const params = {
459
+ client_id: credentials.clientId,
460
+ client_secret: credentials.clientSecret
461
+ };
462
+ if (credentials.authMethod === "client_credentials") {
463
+ params.grant_type = "client_credentials";
464
+ params.scope = "read,write,app:assignable,app:mentionable";
465
+ } else {
466
+ params.grant_type = "refresh_token";
467
+ params.refresh_token = credentials.refreshToken ?? "";
468
+ }
469
+ const tokenData = await fetchToken(params);
470
+ const updated = {
471
+ ...credentials,
472
+ accessToken: tokenData.access_token,
473
+ refreshToken: tokenData.refresh_token ?? credentials.refreshToken,
474
+ tokenExpiresAt: new Date(
475
+ Date.now() + tokenData.expires_in * 1e3
476
+ ).toISOString()
477
+ };
478
+ writeCredentials(agent, credentialsDir, updated);
479
+ const format = getFormat(globalOpts.format);
480
+ printResult(
481
+ {
482
+ data: {
483
+ status: "refreshed",
484
+ agent,
485
+ expiresAt: updated.tokenExpiresAt
486
+ }
487
+ },
488
+ format
489
+ );
490
+ });
491
+ auth.command("revoke").description("Revoke token and delete credentials").action(async (_opts, cmd) => {
492
+ const globalOpts = cmd.optsWithGlobals();
493
+ const agent = globalOpts.agent;
494
+ if (!agent) {
495
+ console.error(
496
+ "Error: --agent is required (or set LINEAR_AGENT_ID env var)"
497
+ );
498
+ process.exit(4);
499
+ }
500
+ const credentialsDir = getCredentialsDir(globalOpts);
501
+ let credentials;
502
+ try {
503
+ credentials = readCredentials(agent, credentialsDir);
504
+ } catch {
505
+ console.error(`No credentials found for agent "${agent}".`);
506
+ process.exit(4);
507
+ }
508
+ try {
509
+ const response = await fetch(
510
+ "https://api.linear.app/oauth/revoke",
511
+ {
512
+ method: "POST",
513
+ headers: {
514
+ "Content-Type": "application/x-www-form-urlencoded",
515
+ Authorization: `Bearer ${credentials.accessToken}`
516
+ }
517
+ }
518
+ );
519
+ if (!response.ok) {
520
+ console.error(
521
+ `Warning: Token revocation returned ${response.status} (token may already be expired)`
522
+ );
523
+ }
524
+ } catch {
525
+ console.error(
526
+ "Warning: Could not reach Linear API to revoke token"
527
+ );
528
+ }
529
+ deleteCredentials(agent, credentialsDir);
530
+ const format = getFormat(globalOpts.format);
531
+ printResult(
532
+ {
533
+ data: {
534
+ status: "revoked",
535
+ agent
536
+ }
537
+ },
538
+ format
539
+ );
540
+ });
541
+ }
542
+
543
+ // src/commands/issue.ts
544
+ var import_fs3 = require("fs");
545
+
546
+ // src/cache.ts
547
+ var import_fs2 = require("fs");
548
+ var import_path2 = require("path");
549
+ var TTL_MS = 24 * 60 * 60 * 1e3;
550
+ function cachePath(agentId, credentialsDir) {
551
+ return (0, import_path2.join)(credentialsDir, `${agentId}.cache.json`);
552
+ }
553
+ function readCache(agentId, credentialsDir) {
554
+ const path = cachePath(agentId, credentialsDir);
555
+ try {
556
+ const raw = (0, import_fs2.readFileSync)(path, "utf-8");
557
+ return JSON.parse(raw);
558
+ } catch {
559
+ return { teams: {} };
560
+ }
561
+ }
562
+ function writeCache(agentId, credentialsDir, cache) {
563
+ (0, import_fs2.mkdirSync)(credentialsDir, { recursive: true });
564
+ const path = cachePath(agentId, credentialsDir);
565
+ (0, import_fs2.writeFileSync)(path, JSON.stringify(cache, null, 2) + "\n");
566
+ }
567
+ function getTeamStates(cache, teamKey) {
568
+ const team = cache.teams[teamKey];
569
+ if (!team) return null;
570
+ const age = Date.now() - new Date(team.updatedAt).getTime();
571
+ if (age > TTL_MS) return null;
572
+ return team.states;
573
+ }
574
+ function setTeamStates(cache, teamKey, states) {
575
+ return {
576
+ ...cache,
577
+ teams: {
578
+ ...cache.teams,
579
+ [teamKey]: {
580
+ states,
581
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
582
+ }
583
+ }
584
+ };
585
+ }
586
+
587
+ // src/resolvers.ts
588
+ var userCache = null;
589
+ function parseTeamKey(issueIdentifier) {
590
+ const match = issueIdentifier.match(/^([A-Z][A-Z0-9]*)-\d+$/);
591
+ if (!match) {
592
+ throw new ValidationError(
593
+ `Cannot parse team key from identifier "${issueIdentifier}". Expected format: TEAM-123`
594
+ );
595
+ }
596
+ return match[1];
597
+ }
598
+ async function resolveUser(value, credentials, client) {
599
+ if (value.toLowerCase() === "me") {
600
+ return credentials.actorId;
601
+ }
602
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
603
+ value
604
+ )) {
605
+ return value;
606
+ }
607
+ if (!userCache) {
608
+ userCache = /* @__PURE__ */ new Map();
609
+ const users = await client.users();
610
+ for (const u of users.nodes) {
611
+ if (u.name) userCache.set(u.name.toLowerCase(), u.id);
612
+ if (u.email) userCache.set(u.email.toLowerCase(), u.id);
613
+ if (u.displayName) userCache.set(u.displayName.toLowerCase(), u.id);
614
+ }
615
+ }
616
+ const id = userCache.get(value.toLowerCase());
617
+ if (id) return id;
618
+ const validOptions = Array.from(userCache.keys());
619
+ throw new ValidationError(
620
+ `No user matching "${value}"`,
621
+ validOptions.slice(0, 20)
622
+ );
623
+ }
624
+ async function resolveState(name, teamKey, client, agentId, credentialsDir) {
625
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
626
+ name
627
+ )) {
628
+ return name;
629
+ }
630
+ let cache = readCache(agentId, credentialsDir);
631
+ let states = getTeamStates(cache, teamKey);
632
+ if (!states) {
633
+ states = await fetchTeamStates(teamKey, client);
634
+ cache = setTeamStates(cache, teamKey, states);
635
+ writeCache(agentId, credentialsDir, cache);
636
+ }
637
+ const nameLower = name.toLowerCase();
638
+ for (const [stateName, stateId] of Object.entries(states)) {
639
+ if (stateName.toLowerCase() === nameLower) return stateId;
640
+ }
641
+ throw new ValidationError(
642
+ `No workflow state "${name}" found for team ${teamKey}`,
643
+ Object.keys(states)
644
+ );
645
+ }
646
+ async function fetchTeamStates(teamKey, client) {
647
+ const teams = await client.teams({ filter: { key: { eq: teamKey } } });
648
+ const team = teams.nodes[0];
649
+ if (!team) {
650
+ throw new ValidationError(`Team "${teamKey}" not found`);
651
+ }
652
+ const workflowStates = await team.states();
653
+ const states = {};
654
+ for (const s of workflowStates.nodes) {
655
+ states[s.name] = s.id;
656
+ }
657
+ return states;
658
+ }
659
+
660
+ // src/client.ts
661
+ var import_sdk2 = require("@linear/sdk");
662
+ async function refreshToken(credentials, agentId, credentialsDir) {
663
+ const body = {
664
+ client_id: credentials.clientId,
665
+ client_secret: credentials.clientSecret
666
+ };
667
+ if (credentials.authMethod === "client_credentials") {
668
+ body.grant_type = "client_credentials";
669
+ body.scope = "read,write,app:assignable,app:mentionable";
670
+ } else {
671
+ body.grant_type = "refresh_token";
672
+ body.refresh_token = credentials.refreshToken ?? "";
673
+ }
674
+ const response = await fetch("https://api.linear.app/oauth/token", {
675
+ method: "POST",
676
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
677
+ body: new URLSearchParams(body).toString()
678
+ });
679
+ if (!response.ok) {
680
+ throw new AuthenticationError(
681
+ `Token refresh failed: ${response.status} ${response.statusText}`
682
+ );
683
+ }
684
+ const data = await response.json();
685
+ const expiresAt = new Date(
686
+ Date.now() + data.expires_in * 1e3
687
+ ).toISOString();
688
+ const updated = {
689
+ ...credentials,
690
+ accessToken: data.access_token,
691
+ tokenExpiresAt: expiresAt,
692
+ refreshToken: data.refresh_token ?? credentials.refreshToken
693
+ };
694
+ writeCredentials(agentId, credentialsDir, updated);
695
+ return updated;
696
+ }
697
+ function isRateLimited(err) {
698
+ const errObj = err;
699
+ const extensions = errObj?.extensions;
700
+ const type = errObj?.type ?? extensions?.code ?? "";
701
+ if (type === "RATELIMITED") {
702
+ const reset = extensions?.rateLimit;
703
+ return reset?.reset ?? true;
704
+ }
705
+ const errors = errObj?.errors;
706
+ if (errors?.some((e) => e.extensions?.code === "RATELIMITED")) {
707
+ return true;
708
+ }
709
+ return false;
710
+ }
711
+ function isAuthError(err) {
712
+ const errObj = err;
713
+ const type = errObj?.type ?? errObj?.extensions?.code;
714
+ if (type === "AUTHENTICATION_ERROR") return true;
715
+ const message = errObj?.message;
716
+ return message?.includes("AUTHENTICATION_ERROR") ?? false;
717
+ }
718
+ function isNetworkError(err) {
719
+ const message = err instanceof Error ? err.message : String(err);
720
+ return message.includes("ECONNREFUSED") || message.includes("ENOTFOUND") || message.includes("ETIMEDOUT") || message.includes("fetch failed") || message.includes("network");
721
+ }
722
+ function sleep(ms) {
723
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
724
+ }
725
+ async function withRetry(fn, credentials, agentId, credentialsDir) {
726
+ const client = createClient(credentials);
727
+ try {
728
+ return await fn(client);
729
+ } catch (err) {
730
+ const rateLimitResult = isRateLimited(err);
731
+ if (rateLimitResult) {
732
+ if (typeof rateLimitResult === "number") {
733
+ const waitMs = Math.max(0, rateLimitResult - Date.now());
734
+ await sleep(Math.min(waitMs, 6e4));
735
+ } else {
736
+ await sleep(5e3);
737
+ }
738
+ try {
739
+ return await fn(client);
740
+ } catch {
741
+ throw new RateLimitError("Rate limited after retry");
742
+ }
743
+ }
744
+ if (isAuthError(err)) {
745
+ try {
746
+ const updated = await refreshToken(
747
+ credentials,
748
+ agentId,
749
+ credentialsDir
750
+ );
751
+ const newClient = createClient(updated);
752
+ return await fn(newClient);
753
+ } catch (refreshErr) {
754
+ if (refreshErr instanceof AuthenticationError) throw refreshErr;
755
+ throw new AuthenticationError(
756
+ "Token refresh failed. Run 'linear auth setup' to re-authenticate."
757
+ );
758
+ }
759
+ }
760
+ if (isNetworkError(err)) {
761
+ await sleep(2e3);
762
+ try {
763
+ return await fn(client);
764
+ } catch {
765
+ throw new NetworkError(
766
+ "Network error after retry. Check connectivity."
767
+ );
768
+ }
769
+ }
770
+ throw classifyError(err);
771
+ }
772
+ }
773
+ function createClient(credentials) {
774
+ return new import_sdk2.LinearClient({ accessToken: credentials.accessToken });
775
+ }
776
+
777
+ // src/context.ts
778
+ function requireAgent(globalOpts) {
779
+ const agent = globalOpts.agent;
780
+ if (!agent) {
781
+ console.error(
782
+ "Error: --agent is required (or set LINEAR_AGENT_ID env var)"
783
+ );
784
+ process.exit(4);
785
+ }
786
+ return agent;
787
+ }
788
+ async function runWithClient(globalOpts, fn) {
789
+ const agentId = requireAgent(globalOpts);
790
+ const credentialsDir = getCredentialsDir(globalOpts);
791
+ const credentials = readCredentials(agentId, credentialsDir);
792
+ return withRetry(
793
+ (client) => fn(client, { credentials, agentId, credentialsDir }),
794
+ credentials,
795
+ agentId,
796
+ credentialsDir
797
+ );
798
+ }
799
+
800
+ // src/commands/issue.ts
801
+ var FREQUENCY_MAP = {
802
+ daily: "days",
803
+ weekly: "weeks",
804
+ monthly: "months",
805
+ yearly: "years"
806
+ };
807
+ function parseDate(value) {
808
+ const durationMatch = value.match(/^-P(\d+)D$/);
809
+ if (durationMatch) {
810
+ const days = parseInt(durationMatch[1], 10);
811
+ const date = new Date(Date.now() - days * 24 * 60 * 60 * 1e3);
812
+ return date.toISOString();
813
+ }
814
+ return new Date(value).toISOString();
815
+ }
816
+ async function createRelations(client, issueId, blocks, blockedBy, relatedTo) {
817
+ const succeeded = [];
818
+ const failed = [];
819
+ const warnings = [];
820
+ const relations = [];
821
+ for (const targetId of blocks) {
822
+ relations.push({
823
+ relatedIssueId: targetId,
824
+ type: "blocks",
825
+ label: `blocks ${targetId}`
826
+ });
827
+ }
828
+ for (const targetId of blockedBy) {
829
+ relations.push({
830
+ relatedIssueId: targetId,
831
+ type: "blocks",
832
+ label: `blocked-by ${targetId}`
833
+ });
834
+ }
835
+ for (const targetId of relatedTo) {
836
+ relations.push({
837
+ relatedIssueId: targetId,
838
+ type: "related",
839
+ label: `related-to ${targetId}`
840
+ });
841
+ }
842
+ for (const rel of relations) {
843
+ try {
844
+ if (rel.label.startsWith("blocked-by")) {
845
+ await client.createIssueRelation({
846
+ issueId: rel.relatedIssueId,
847
+ relatedIssueId: issueId,
848
+ type: rel.type
849
+ });
850
+ } else {
851
+ await client.createIssueRelation({
852
+ issueId,
853
+ relatedIssueId: rel.relatedIssueId,
854
+ type: rel.type
855
+ });
856
+ }
857
+ succeeded.push(rel.label);
858
+ } catch (err) {
859
+ const msg = err instanceof Error ? err.message : String(err);
860
+ failed.push(rel.label);
861
+ warnings.push(`Failed to create relation "${rel.label}": ${msg}`);
862
+ }
863
+ }
864
+ return { succeeded, failed, warnings };
865
+ }
866
+ function collectArray(value, previous) {
867
+ return previous.concat([value]);
868
+ }
869
+ async function resolveLabels(client, labels) {
870
+ const labelIds = [];
871
+ for (const l of labels) {
872
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(l)) {
873
+ labelIds.push(l);
874
+ continue;
875
+ }
876
+ const result = await client.issueLabels({
877
+ filter: { name: { eqIgnoreCase: l } }
878
+ });
879
+ if (!result.nodes[0]) {
880
+ throw new ValidationError(`No label matching "${l}"`);
881
+ }
882
+ labelIds.push(result.nodes[0].id);
883
+ }
884
+ return labelIds;
885
+ }
886
+ async function resolveProject(client, project) {
887
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(project)) {
888
+ return project;
889
+ }
890
+ const projects = await client.projects({
891
+ filter: { name: { eqIgnoreCase: project } }
892
+ });
893
+ if (!projects.nodes[0]) {
894
+ throw new ValidationError(`No project matching "${project}"`);
895
+ }
896
+ return projects.nodes[0].id;
897
+ }
898
+ async function resolveTeam(client, team) {
899
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(team)) {
900
+ return team;
901
+ }
902
+ const teams = await client.teams({
903
+ filter: { name: { eqIgnoreCase: team } }
904
+ });
905
+ if (!teams.nodes[0]) {
906
+ throw new ValidationError(`No team matching "${team}"`);
907
+ }
908
+ return teams.nodes[0].id;
909
+ }
910
+ function registerIssueCommands(program2) {
911
+ const issue = program2.command("issue").description("Create, read, update, and search issues");
912
+ issue.command("list").description("List issues with filters").option("--assignee <user>", "Filter by assignee (name, email, ID, or 'me')").option("--delegate <agent>", "Filter by delegated agent").option("--state <state>", "Filter by workflow state name or type").option("--label <label>", "Filter by label name").option("--team <team>", "Filter by team name or ID").option("--project <project>", "Filter by project name or ID").option("--priority <priority>", "Filter by priority (0-4)").option("--query <text>", "Search title/description").option("--created-after <date>", "ISO-8601 date or duration (e.g., -P7D)").option("--updated-after <date>", "ISO-8601 date or duration").option("--limit <n>", "Max results (default: 50, max: 250)", "50").option("--include-archived", "Include archived issues").action(async (opts, cmd) => {
913
+ const globalOpts = cmd.optsWithGlobals();
914
+ await runWithClient(globalOpts, async (client, { credentials }) => {
915
+ const format = getFormat(globalOpts.format);
916
+ const filter = {};
917
+ if (opts.assignee) {
918
+ const userId = await resolveUser(opts.assignee, credentials, client);
919
+ filter.assignee = { id: { eq: userId } };
920
+ }
921
+ if (opts.delegate) {
922
+ const userId = await resolveUser(opts.delegate, credentials, client);
923
+ filter.delegate = { id: { eq: userId } };
924
+ }
925
+ if (opts.state) {
926
+ filter.state = { name: { eqIgnoreCase: opts.state } };
927
+ }
928
+ if (opts.label) {
929
+ filter.labels = { name: { eqIgnoreCase: opts.label } };
930
+ }
931
+ if (opts.team) {
932
+ filter.team = { name: { eqIgnoreCase: opts.team } };
933
+ }
934
+ if (opts.project) {
935
+ filter.project = { name: { eqIgnoreCase: opts.project } };
936
+ }
937
+ if (opts.priority) {
938
+ filter.priority = { eq: parseInt(opts.priority, 10) };
939
+ }
940
+ if (opts.createdAfter) {
941
+ filter.createdAt = { gte: parseDate(opts.createdAfter) };
942
+ }
943
+ if (opts.updatedAfter) {
944
+ filter.updatedAt = { gte: parseDate(opts.updatedAfter) };
945
+ }
946
+ const limit = Math.min(parseInt(opts.limit, 10) || 50, 250);
947
+ const queryOpts = {
948
+ filter,
949
+ first: limit,
950
+ includeArchived: opts.includeArchived ?? false
951
+ };
952
+ if (opts.query) {
953
+ filter.or = [
954
+ { title: { containsIgnoreCase: opts.query } },
955
+ { description: { containsIgnoreCase: opts.query } }
956
+ ];
957
+ }
958
+ const issues = await client.issues(queryOpts);
959
+ const results = await Promise.all(
960
+ issues.nodes.map(async (i) => {
961
+ const state = await i.state;
962
+ return {
963
+ id: i.identifier,
964
+ title: i.title,
965
+ state: state?.name ?? null,
966
+ priority: i.priority,
967
+ url: i.url
968
+ };
969
+ })
970
+ );
971
+ printResult({ data: results }, format);
972
+ });
973
+ });
974
+ issue.command("get").description("Get full issue details").argument("<ids...>", "Issue identifier(s) (e.g., MAIN-42 MAIN-43)").action(async (ids, _opts, cmd) => {
975
+ const globalOpts = cmd.optsWithGlobals();
976
+ await runWithClient(globalOpts, async (client) => {
977
+ const format = getFormat(globalOpts.format);
978
+ const fetchIssue = async (id) => {
979
+ const issueObj = await client.issue(id);
980
+ const [state, assignee, delegate, labels, parent, children, comments, relations] = await Promise.all([
981
+ issueObj.state,
982
+ issueObj.assignee,
983
+ issueObj.delegate,
984
+ issueObj.labels(),
985
+ issueObj.parent,
986
+ issueObj.children(),
987
+ issueObj.comments(),
988
+ issueObj.relations()
989
+ ]);
990
+ return {
991
+ id: issueObj.identifier,
992
+ title: issueObj.title,
993
+ description: issueObj.description ?? null,
994
+ state: state?.name ?? null,
995
+ stateType: state?.type ?? null,
996
+ assignee: assignee ? { id: assignee.id, name: assignee.name } : null,
997
+ delegate: delegate ? { id: delegate.id, name: delegate.name } : null,
998
+ labels: labels.nodes.map((l) => ({ id: l.id, name: l.name })),
999
+ priority: issueObj.priority,
1000
+ priorityLabel: issueObj.priorityLabel,
1001
+ parent: parent ? { id: parent.identifier, title: parent.title } : null,
1002
+ children: children.nodes.map((c) => ({
1003
+ id: c.identifier,
1004
+ title: c.title
1005
+ })),
1006
+ relations: relations.nodes.map((r) => ({
1007
+ type: r.type,
1008
+ relatedIssueId: r.relatedIssueId ?? null
1009
+ })),
1010
+ comments: comments.nodes.map((c) => ({
1011
+ id: c.id,
1012
+ body: c.body,
1013
+ createdAt: c.createdAt
1014
+ })),
1015
+ dueDate: issueObj.dueDate ?? null,
1016
+ estimate: issueObj.estimate ?? null,
1017
+ url: issueObj.url
1018
+ };
1019
+ };
1020
+ if (ids.length === 1) {
1021
+ const result = await fetchIssue(ids[0]);
1022
+ printResult({ data: result }, format);
1023
+ } else {
1024
+ const settled = await Promise.allSettled(ids.map(fetchIssue));
1025
+ const results = [];
1026
+ const warnings = [];
1027
+ for (let i = 0; i < settled.length; i++) {
1028
+ const outcome = settled[i];
1029
+ if (outcome.status === "fulfilled") {
1030
+ results.push(outcome.value);
1031
+ } else {
1032
+ const msg = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
1033
+ warnings.push(`${ids[i]}: ${msg}`);
1034
+ }
1035
+ }
1036
+ printResult({ data: results, warnings: warnings.length ? warnings : void 0 }, format);
1037
+ }
1038
+ });
1039
+ });
1040
+ issue.command("create").description("Create a new issue").requiredOption("--title <text>", "Issue title").requiredOption("--team <team>", "Team name or ID").option("--description <text>", "Markdown description").option("--description-file <path>", "Read description from file").option("--assignee <user>", "Assign to user").option("--delegate <agent>", "Delegate to agent").option("--state <state>", "Initial workflow state").option("--label <label>", "Add label (repeatable)", collectArray, []).option("--priority <priority>", "Priority level (0-4)").option("--project <project>", "Add to project").option("--parent <id>", "Set parent issue").option("--blocks <id>", "This issue blocks <id> (repeatable)", collectArray, []).option("--blocked-by <id>", "This issue is blocked by <id> (repeatable)", collectArray, []).option("--related-to <id>", "Related issue (repeatable)", collectArray, []).option("--due-date <date>", "Due date (ISO format)").option("--estimate <n>", "Effort estimate").action(async (opts, cmd) => {
1041
+ const globalOpts = cmd.optsWithGlobals();
1042
+ await runWithClient(globalOpts, async (client, { credentials, agentId, credentialsDir }) => {
1043
+ const format = getFormat(globalOpts.format);
1044
+ const teamId = await resolveTeam(client, opts.team);
1045
+ const input = {
1046
+ title: opts.title,
1047
+ teamId
1048
+ };
1049
+ if (opts.descriptionFile) {
1050
+ input.description = (0, import_fs3.readFileSync)(opts.descriptionFile, "utf-8");
1051
+ } else if (opts.description) {
1052
+ input.description = opts.description;
1053
+ } else if (!process.stdin.isTTY) {
1054
+ try {
1055
+ const stdinContent = (0, import_fs3.readFileSync)(0, "utf-8").trim();
1056
+ if (stdinContent) input.description = stdinContent;
1057
+ } catch (err) {
1058
+ const message = err instanceof Error ? err.message : String(err);
1059
+ console.error(`Error: Failed to read from stdin: ${message}`);
1060
+ process.exit(4);
1061
+ }
1062
+ }
1063
+ if (opts.assignee) {
1064
+ input.assigneeId = await resolveUser(
1065
+ opts.assignee,
1066
+ credentials,
1067
+ client
1068
+ );
1069
+ }
1070
+ if (opts.delegate) {
1071
+ input.delegateId = await resolveUser(
1072
+ opts.delegate,
1073
+ credentials,
1074
+ client
1075
+ );
1076
+ }
1077
+ if (opts.state) {
1078
+ const team = await client.team(teamId);
1079
+ input.stateId = await resolveState(
1080
+ opts.state,
1081
+ team.key,
1082
+ client,
1083
+ agentId,
1084
+ credentialsDir
1085
+ );
1086
+ }
1087
+ if (opts.label && opts.label.length > 0) {
1088
+ input.labelIds = await resolveLabels(client, opts.label);
1089
+ }
1090
+ if (opts.priority !== void 0) {
1091
+ input.priority = parseInt(opts.priority, 10);
1092
+ }
1093
+ if (opts.project) {
1094
+ input.projectId = await resolveProject(client, opts.project);
1095
+ }
1096
+ if (opts.parent) {
1097
+ input.parentId = opts.parent;
1098
+ }
1099
+ if (opts.dueDate) {
1100
+ input.dueDate = opts.dueDate;
1101
+ }
1102
+ if (opts.estimate !== void 0) {
1103
+ input.estimate = parseInt(opts.estimate, 10);
1104
+ }
1105
+ const payload = await client.createIssue(input);
1106
+ const created = await payload.issue;
1107
+ const result = {
1108
+ id: created?.identifier ?? null,
1109
+ title: created?.title ?? opts.title,
1110
+ url: created?.url ?? null
1111
+ };
1112
+ const blocks = opts.blocks ?? [];
1113
+ const blockedBy = opts.blockedBy ?? [];
1114
+ const relatedTo = opts.relatedTo ?? [];
1115
+ const hasRelations = blocks.length > 0 || blockedBy.length > 0 || relatedTo.length > 0;
1116
+ if (hasRelations && created) {
1117
+ const { succeeded, failed, warnings } = await createRelations(
1118
+ client,
1119
+ created.id,
1120
+ blocks,
1121
+ blockedBy,
1122
+ relatedTo
1123
+ );
1124
+ if (failed.length > 0) {
1125
+ result.relations = { succeeded, failed };
1126
+ printResult({ data: result, warnings }, format);
1127
+ throw new PartialSuccessError(
1128
+ `Issue created but some relations failed`,
1129
+ succeeded,
1130
+ failed
1131
+ );
1132
+ }
1133
+ if (succeeded.length > 0) {
1134
+ result.relations = { succeeded };
1135
+ }
1136
+ }
1137
+ printResult({ data: result }, format);
1138
+ });
1139
+ });
1140
+ issue.command("update").description("Update an existing issue").argument("<id>", "Issue identifier").option("--title <text>", "Issue title").option("--description <text>", "Markdown description").option("--description-file <path>", "Read description from file").option("--assignee <user>", 'Assign to user (pass "null" to clear)').option("--delegate <agent>", 'Delegate to agent (pass "null" to clear)').option("--state <state>", "Workflow state").option("--label <label>", "Add label (repeatable)", collectArray, []).option("--priority <priority>", "Priority level (0-4)").option("--project <project>", "Add to project").option("--parent <id>", 'Set parent issue (pass "null" to clear)').option("--blocks <id>", "This issue blocks <id> (repeatable)", collectArray, []).option("--blocked-by <id>", "This issue is blocked by <id> (repeatable)", collectArray, []).option("--related-to <id>", "Related issue (repeatable)", collectArray, []).option("--due-date <date>", "Due date (ISO format)").option("--estimate <n>", "Effort estimate").action(async (id, opts, cmd) => {
1141
+ const globalOpts = cmd.optsWithGlobals();
1142
+ await runWithClient(globalOpts, async (client, { credentials, agentId, credentialsDir }) => {
1143
+ const format = getFormat(globalOpts.format);
1144
+ const input = {};
1145
+ if (opts.title) {
1146
+ input.title = opts.title;
1147
+ }
1148
+ if (opts.descriptionFile) {
1149
+ input.description = (0, import_fs3.readFileSync)(opts.descriptionFile, "utf-8");
1150
+ } else if (opts.description) {
1151
+ input.description = opts.description;
1152
+ } else if (!process.stdin.isTTY) {
1153
+ try {
1154
+ const stdinContent = (0, import_fs3.readFileSync)(0, "utf-8").trim();
1155
+ if (stdinContent) input.description = stdinContent;
1156
+ } catch (err) {
1157
+ const message = err instanceof Error ? err.message : String(err);
1158
+ console.error(`Error: Failed to read from stdin: ${message}`);
1159
+ process.exit(4);
1160
+ }
1161
+ }
1162
+ if (opts.assignee !== void 0) {
1163
+ if (opts.assignee === "null") {
1164
+ input.assigneeId = null;
1165
+ } else {
1166
+ input.assigneeId = await resolveUser(
1167
+ opts.assignee,
1168
+ credentials,
1169
+ client
1170
+ );
1171
+ }
1172
+ }
1173
+ if (opts.delegate !== void 0) {
1174
+ if (opts.delegate === "null") {
1175
+ input.delegateId = null;
1176
+ } else {
1177
+ input.delegateId = await resolveUser(
1178
+ opts.delegate,
1179
+ credentials,
1180
+ client
1181
+ );
1182
+ }
1183
+ }
1184
+ if (opts.state) {
1185
+ const teamKey = parseTeamKey(id);
1186
+ input.stateId = await resolveState(
1187
+ opts.state,
1188
+ teamKey,
1189
+ client,
1190
+ agentId,
1191
+ credentialsDir
1192
+ );
1193
+ }
1194
+ if (opts.label && opts.label.length > 0) {
1195
+ input.labelIds = await resolveLabels(client, opts.label);
1196
+ }
1197
+ if (opts.priority !== void 0) {
1198
+ input.priority = parseInt(opts.priority, 10);
1199
+ }
1200
+ if (opts.project) {
1201
+ input.projectId = await resolveProject(client, opts.project);
1202
+ }
1203
+ if (opts.parent !== void 0) {
1204
+ if (opts.parent === "null") {
1205
+ input.parentId = null;
1206
+ } else {
1207
+ input.parentId = opts.parent;
1208
+ }
1209
+ }
1210
+ if (opts.dueDate) {
1211
+ input.dueDate = opts.dueDate;
1212
+ }
1213
+ if (opts.estimate !== void 0) {
1214
+ input.estimate = parseInt(opts.estimate, 10);
1215
+ }
1216
+ const payload = await client.updateIssue(id, input);
1217
+ const updated = await payload.issue;
1218
+ const result = {
1219
+ id: updated?.identifier ?? id,
1220
+ title: updated?.title ?? null,
1221
+ url: updated?.url ?? null
1222
+ };
1223
+ const blocks = opts.blocks ?? [];
1224
+ const blockedBy = opts.blockedBy ?? [];
1225
+ const relatedTo = opts.relatedTo ?? [];
1226
+ const hasRelations = blocks.length > 0 || blockedBy.length > 0 || relatedTo.length > 0;
1227
+ if (hasRelations && updated) {
1228
+ const { succeeded, failed, warnings } = await createRelations(
1229
+ client,
1230
+ updated.id,
1231
+ blocks,
1232
+ blockedBy,
1233
+ relatedTo
1234
+ );
1235
+ if (failed.length > 0) {
1236
+ result.relations = { succeeded, failed };
1237
+ printResult({ data: result, warnings }, format);
1238
+ throw new PartialSuccessError(
1239
+ `Issue updated but some relations failed`,
1240
+ succeeded,
1241
+ failed
1242
+ );
1243
+ }
1244
+ if (succeeded.length > 0) {
1245
+ result.relations = { succeeded };
1246
+ }
1247
+ }
1248
+ printResult({ data: result }, format);
1249
+ });
1250
+ });
1251
+ issue.command("transition").description("Move issue to a workflow state by name").argument("<id>", "Issue identifier (e.g., MAIN-42)").argument("<state>", "Target workflow state name").action(async (id, state, _opts, cmd) => {
1252
+ const globalOpts = cmd.optsWithGlobals();
1253
+ await runWithClient(globalOpts, async (client, { agentId, credentialsDir }) => {
1254
+ const format = getFormat(globalOpts.format);
1255
+ const teamKey = parseTeamKey(id);
1256
+ const stateId = await resolveState(
1257
+ state,
1258
+ teamKey,
1259
+ client,
1260
+ agentId,
1261
+ credentialsDir
1262
+ );
1263
+ const payload = await client.updateIssue(id, { stateId });
1264
+ const updated = await payload.issue;
1265
+ printResult(
1266
+ {
1267
+ data: {
1268
+ id: updated?.identifier ?? id,
1269
+ state,
1270
+ url: updated?.url ?? null
1271
+ }
1272
+ },
1273
+ format
1274
+ );
1275
+ });
1276
+ });
1277
+ issue.command("search").description("Full-text search via searchIssues").argument("<query>", "Search query").option("--team <team>", "Boost results for a specific team").option("--include-comments", "Search within comment content").option("--include-archived", "Include archived issues in results").action(async (query, opts, cmd) => {
1278
+ const globalOpts = cmd.optsWithGlobals();
1279
+ await runWithClient(globalOpts, async (client) => {
1280
+ const format = getFormat(globalOpts.format);
1281
+ const searchOpts = {};
1282
+ if (opts.team) {
1283
+ searchOpts.teamId = await resolveTeam(client, opts.team);
1284
+ }
1285
+ if (opts.includeComments) {
1286
+ searchOpts.includeComments = true;
1287
+ }
1288
+ if (opts.includeArchived) {
1289
+ searchOpts.includeArchived = true;
1290
+ }
1291
+ const results = await client.searchIssues(query, searchOpts);
1292
+ const items = results.nodes.map((i) => ({
1293
+ id: i.identifier,
1294
+ title: i.title,
1295
+ url: i.url
1296
+ }));
1297
+ printResult({ data: items }, format);
1298
+ });
1299
+ });
1300
+ issue.command("archive").description("Archive an issue").argument("<id>", "Issue identifier").action(async (id, _opts, cmd) => {
1301
+ const globalOpts = cmd.optsWithGlobals();
1302
+ await runWithClient(globalOpts, async (client) => {
1303
+ const format = getFormat(globalOpts.format);
1304
+ await client.archiveIssue(id);
1305
+ printResult(
1306
+ {
1307
+ data: {
1308
+ id,
1309
+ status: "archived"
1310
+ }
1311
+ },
1312
+ format
1313
+ );
1314
+ });
1315
+ });
1316
+ issue.command("delete").description("Delete an issue").argument("<id>", "Issue identifier").action(async (id, _opts, cmd) => {
1317
+ const globalOpts = cmd.optsWithGlobals();
1318
+ await runWithClient(globalOpts, async (client) => {
1319
+ const format = getFormat(globalOpts.format);
1320
+ await client.deleteIssue(id);
1321
+ printResult(
1322
+ {
1323
+ data: {
1324
+ id,
1325
+ status: "deleted"
1326
+ }
1327
+ },
1328
+ format
1329
+ );
1330
+ });
1331
+ });
1332
+ const schedule = issue.command("schedule").description("Manage recurring issue schedules");
1333
+ schedule.command("create").description("Create a recurring issue schedule").requiredOption("--title <text>", "Issue title").requiredOption("--team <team>", "Team name or ID").requiredOption(
1334
+ "--frequency <freq>",
1335
+ "Recurrence frequency (daily, weekly, monthly, yearly)"
1336
+ ).requiredOption("--start-at <date>", "Start date (ISO format, e.g. 2026-03-01)").option("--interval <n>", "Recurrence interval (default: 1)", "1").option("--description <text>", "Issue description").option("--state <state>", "Initial workflow state").option("--assignee <user>", "Assign to user").option("--priority <n>", "Priority (0-4)").action(async (opts, cmd) => {
1337
+ const globalOpts = cmd.optsWithGlobals();
1338
+ await runWithClient(globalOpts, async (client, { credentials, agentId, credentialsDir }) => {
1339
+ const format = getFormat(globalOpts.format);
1340
+ const scheduleType = FREQUENCY_MAP[opts.frequency];
1341
+ if (!scheduleType) {
1342
+ throw new ValidationError(
1343
+ `Invalid frequency "${opts.frequency}"`,
1344
+ Object.keys(FREQUENCY_MAP)
1345
+ );
1346
+ }
1347
+ const teamId = await resolveTeam(client, opts.team);
1348
+ const interval = parseInt(opts.interval, 10);
1349
+ if (isNaN(interval) || interval < 1) {
1350
+ throw new ValidationError(
1351
+ `Invalid interval "${opts.interval}". Expected a positive integer.`
1352
+ );
1353
+ }
1354
+ const templateData = {
1355
+ title: opts.title,
1356
+ teamId,
1357
+ schedule: {
1358
+ startAt: opts.startAt,
1359
+ interval,
1360
+ type: scheduleType
1361
+ }
1362
+ };
1363
+ if (opts.description) templateData.description = opts.description;
1364
+ if (opts.priority !== void 0) {
1365
+ const priority = parseInt(opts.priority, 10);
1366
+ if (isNaN(priority) || priority < 0 || priority > 4) {
1367
+ throw new ValidationError(
1368
+ `Invalid priority "${opts.priority}". Expected an integer between 0 and 4.`
1369
+ );
1370
+ }
1371
+ templateData.priority = priority;
1372
+ }
1373
+ if (opts.state) {
1374
+ const team = await client.team(teamId);
1375
+ templateData.stateId = await resolveState(
1376
+ opts.state,
1377
+ team.key,
1378
+ client,
1379
+ agentId,
1380
+ credentialsDir
1381
+ );
1382
+ }
1383
+ if (opts.assignee) {
1384
+ templateData.assigneeId = await resolveUser(opts.assignee, credentials, client);
1385
+ }
1386
+ const payload = await client.createTemplate({
1387
+ type: "recurringIssue",
1388
+ name: opts.title,
1389
+ teamId,
1390
+ templateData: JSON.stringify(templateData)
1391
+ });
1392
+ const template = await payload.template;
1393
+ printResult({ data: { id: template.id, name: template.name } }, format);
1394
+ });
1395
+ });
1396
+ schedule.command("list").description("List recurring issue schedules").option("--team <team>", "Filter by team name or ID").action(async (opts, cmd) => {
1397
+ const globalOpts = cmd.optsWithGlobals();
1398
+ await runWithClient(globalOpts, async (client) => {
1399
+ const format = getFormat(globalOpts.format);
1400
+ const teamId = opts.team ? await resolveTeam(client, opts.team) : null;
1401
+ const allTemplates = await client.templates;
1402
+ const filtered = await Promise.all(
1403
+ allTemplates.filter((t) => t.type === "recurringIssue").map(async (t) => {
1404
+ const team = await t.team;
1405
+ return { template: t, teamId: team?.id ?? null };
1406
+ })
1407
+ );
1408
+ const results = filtered.filter(({ teamId: tid }) => teamId === null || tid === teamId).map(({ template: t }) => {
1409
+ const data = typeof t.templateData === "string" ? (() => {
1410
+ try {
1411
+ return JSON.parse(t.templateData);
1412
+ } catch {
1413
+ return {};
1414
+ }
1415
+ })() : t.templateData ?? {};
1416
+ return { id: t.id, name: t.name, schedule: data.schedule ?? null };
1417
+ });
1418
+ printResult({ data: results }, format);
1419
+ });
1420
+ });
1421
+ schedule.command("delete").description("Delete a recurring issue schedule").argument("<id>", "Template UUID").action(async (id, _opts, cmd) => {
1422
+ const globalOpts = cmd.optsWithGlobals();
1423
+ await runWithClient(globalOpts, async (client) => {
1424
+ const format = getFormat(globalOpts.format);
1425
+ await client.deleteTemplate(id);
1426
+ printResult({ data: { id, status: "deleted" } }, format);
1427
+ });
1428
+ });
1429
+ }
1430
+
1431
+ // src/commands/comment.ts
1432
+ var import_fs4 = require("fs");
1433
+ function registerCommentCommands(program2) {
1434
+ const comment = program2.command("comment").description("Add and list comments on issues");
1435
+ comment.command("list").description("List all comments on an issue").argument("<issue-id>", "Issue identifier (e.g., MAIN-42)").action(async (issueId, _opts, cmd) => {
1436
+ const globalOpts = cmd.optsWithGlobals();
1437
+ await runWithClient(globalOpts, async (client) => {
1438
+ const issue = await client.issue(issueId);
1439
+ const commentsConnection = await issue.comments();
1440
+ const comments = [];
1441
+ for (const c of commentsConnection.nodes) {
1442
+ const user = await c.user;
1443
+ comments.push({
1444
+ id: c.id,
1445
+ author: user?.name ?? user?.id ?? "Unknown",
1446
+ body: c.body,
1447
+ createdAt: c.createdAt,
1448
+ parentId: c.parentId ?? null
1449
+ });
1450
+ }
1451
+ const format = getFormat(globalOpts.format);
1452
+ printResult({ data: comments }, format);
1453
+ });
1454
+ });
1455
+ comment.command("add").description("Add a comment to an issue").argument("<issue-id>", "Issue identifier (e.g., MAIN-42)").option("--body <text>", "Comment body as markdown").option("--body-file <path>", "Read body from file").option("--reply-to <comment-id>", "Reply to a specific comment").action(async (issueId, opts, cmd) => {
1456
+ const globalOpts = cmd.optsWithGlobals();
1457
+ await runWithClient(globalOpts, async (client) => {
1458
+ let body = opts.body;
1459
+ if (opts.bodyFile) {
1460
+ body = (0, import_fs4.readFileSync)(opts.bodyFile, "utf-8");
1461
+ } else if (!body && !process.stdin.isTTY) {
1462
+ try {
1463
+ const stdinContent = (0, import_fs4.readFileSync)(0, "utf-8").trim();
1464
+ if (stdinContent) body = stdinContent;
1465
+ } catch (err) {
1466
+ const message = err instanceof Error ? err.message : String(err);
1467
+ console.error(`Error: Failed to read from stdin: ${message}`);
1468
+ process.exit(4);
1469
+ }
1470
+ }
1471
+ if (!body) {
1472
+ console.error("Error: --body, --body-file, or stdin pipe is required");
1473
+ process.exit(4);
1474
+ }
1475
+ const input = { issueId, body };
1476
+ if (opts.replyTo) {
1477
+ input.parentId = opts.replyTo;
1478
+ }
1479
+ const result = await client.createComment(input);
1480
+ const commentNode = await result.comment;
1481
+ const format = getFormat(globalOpts.format);
1482
+ printResult(
1483
+ {
1484
+ data: {
1485
+ id: commentNode?.id,
1486
+ issueId,
1487
+ body,
1488
+ parentId: opts.replyTo ?? null,
1489
+ success: result.success
1490
+ }
1491
+ },
1492
+ format
1493
+ );
1494
+ });
1495
+ });
1496
+ comment.command("update").description("Update an existing comment").argument("<comment-id>", "Comment ID").option("--body <text>", "Updated comment body").option("--body-file <path>", "Read body from file").action(async (commentId, opts, cmd) => {
1497
+ const globalOpts = cmd.optsWithGlobals();
1498
+ await runWithClient(globalOpts, async (client) => {
1499
+ let body = opts.body;
1500
+ if (opts.bodyFile) {
1501
+ body = (0, import_fs4.readFileSync)(opts.bodyFile, "utf-8");
1502
+ } else if (!body && !process.stdin.isTTY) {
1503
+ try {
1504
+ const stdinContent = (0, import_fs4.readFileSync)(0, "utf-8").trim();
1505
+ if (stdinContent) body = stdinContent;
1506
+ } catch (err) {
1507
+ const message = err instanceof Error ? err.message : String(err);
1508
+ console.error(`Error: Failed to read from stdin: ${message}`);
1509
+ process.exit(4);
1510
+ }
1511
+ }
1512
+ if (!body) {
1513
+ console.error("Error: --body, --body-file, or stdin pipe is required");
1514
+ process.exit(4);
1515
+ }
1516
+ const result = await client.updateComment(commentId, { body });
1517
+ const format = getFormat(globalOpts.format);
1518
+ printResult(
1519
+ {
1520
+ data: {
1521
+ id: commentId,
1522
+ body,
1523
+ success: result.success
1524
+ }
1525
+ },
1526
+ format
1527
+ );
1528
+ });
1529
+ });
1530
+ }
1531
+
1532
+ // src/commands/inbox.ts
1533
+ var VALID_CATEGORIES = [
1534
+ "assignments",
1535
+ "mentions",
1536
+ "statusChanges",
1537
+ "commentsAndReplies",
1538
+ "reactions",
1539
+ "reviews",
1540
+ "appsAndIntegrations",
1541
+ "triage",
1542
+ "system"
1543
+ ];
1544
+ function parseSinceDate(since) {
1545
+ const durationMatch = since.match(
1546
+ /^-?P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/i
1547
+ );
1548
+ if (durationMatch) {
1549
+ const days = parseInt(durationMatch[1] ?? "0", 10);
1550
+ const hours = parseInt(durationMatch[2] ?? "0", 10);
1551
+ const minutes = parseInt(durationMatch[3] ?? "0", 10);
1552
+ const seconds = parseInt(durationMatch[4] ?? "0", 10);
1553
+ const ms = (days * 86400 + hours * 3600 + minutes * 60 + seconds) * 1e3;
1554
+ return new Date(Date.now() - ms);
1555
+ }
1556
+ const date = new Date(since);
1557
+ if (isNaN(date.getTime())) {
1558
+ throw new Error(`Invalid date or duration: ${since}`);
1559
+ }
1560
+ return date;
1561
+ }
1562
+ function registerInboxCommands(program2) {
1563
+ const inbox = program2.command("inbox").description("View and manage inbox notifications");
1564
+ inbox.command("list", { isDefault: true }).description("List notifications").option("--include-archived", "Show all notifications (not just unprocessed)").option("--type <type>", "Filter by notification type string").option("--category <category>", "Filter by notification category").option("--since <date>", "Only notifications after this date or duration").action(async (opts, cmd) => {
1565
+ const globalOpts = cmd.optsWithGlobals();
1566
+ if (opts.category && !VALID_CATEGORIES.includes(opts.category)) {
1567
+ console.error(
1568
+ `Error: Invalid category "${opts.category}". Valid categories: ${VALID_CATEGORIES.join(", ")}`
1569
+ );
1570
+ process.exit(4);
1571
+ }
1572
+ await runWithClient(globalOpts, async (client) => {
1573
+ const filter = {};
1574
+ if (opts.type) {
1575
+ filter.type = { eq: opts.type };
1576
+ }
1577
+ const notificationsConnection = await client.notifications({
1578
+ filter: Object.keys(filter).length > 0 ? filter : void 0
1579
+ });
1580
+ let notifications = notificationsConnection.nodes;
1581
+ if (!opts.includeArchived) {
1582
+ notifications = notifications.filter(
1583
+ (n) => !n.archivedAt
1584
+ );
1585
+ }
1586
+ if (opts.category) {
1587
+ notifications = notifications.filter(
1588
+ (n) => n.category === opts.category
1589
+ );
1590
+ }
1591
+ if (opts.since) {
1592
+ const sinceDate = parseSinceDate(opts.since);
1593
+ notifications = notifications.filter(
1594
+ (n) => new Date(n.createdAt) >= sinceDate
1595
+ );
1596
+ }
1597
+ const results = [];
1598
+ for (const n of notifications) {
1599
+ const issue = await n.issue;
1600
+ results.push({
1601
+ id: n.id,
1602
+ type: n.type,
1603
+ createdAt: n.createdAt,
1604
+ archivedAt: n.archivedAt ?? null,
1605
+ issue: issue ? { id: issue.id, identifier: issue.identifier, title: issue.title, url: issue.url } : null
1606
+ });
1607
+ }
1608
+ const format = getFormat(globalOpts.format);
1609
+ printResult({ data: results }, format);
1610
+ });
1611
+ });
1612
+ inbox.command("dismiss").description("Dismiss (archive) a notification").argument("<id>", "Notification ID").action(async (id, _opts, cmd) => {
1613
+ const globalOpts = cmd.optsWithGlobals();
1614
+ await runWithClient(globalOpts, async (client) => {
1615
+ const result = await client.archiveNotification(id);
1616
+ const format = getFormat(globalOpts.format);
1617
+ printResult(
1618
+ {
1619
+ data: {
1620
+ id,
1621
+ status: "dismissed",
1622
+ success: result.success
1623
+ }
1624
+ },
1625
+ format
1626
+ );
1627
+ });
1628
+ });
1629
+ inbox.command("dismiss-all").description("Dismiss all unprocessed notifications").action(async (_opts, cmd) => {
1630
+ const globalOpts = cmd.optsWithGlobals();
1631
+ await runWithClient(globalOpts, async (client) => {
1632
+ const notificationsConnection = await client.notifications();
1633
+ const unarchived = notificationsConnection.nodes.filter(
1634
+ (n) => !n.archivedAt
1635
+ );
1636
+ let dismissed = 0;
1637
+ for (const n of unarchived) {
1638
+ await client.archiveNotification(n.id);
1639
+ dismissed++;
1640
+ }
1641
+ const format = getFormat(globalOpts.format);
1642
+ printResult(
1643
+ {
1644
+ data: {
1645
+ status: "dismissed-all",
1646
+ count: dismissed
1647
+ }
1648
+ },
1649
+ format
1650
+ );
1651
+ });
1652
+ });
1653
+ }
1654
+
1655
+ // src/commands/delegate.ts
1656
+ function registerDelegateCommands(program2) {
1657
+ const delegate = program2.command("delegate").description("Delegation shortcuts for assigning agents to issues");
1658
+ delegate.command("assign").description("Delegate an issue to an agent").argument("<issue-id>", "Issue identifier (e.g., TEAM-123)").requiredOption("--to <agent>", "Agent name, email, ID, or 'me'").action(async (issueId, opts, cmd) => {
1659
+ const globalOpts = cmd.optsWithGlobals();
1660
+ await runWithClient(globalOpts, async (client, { credentials }) => {
1661
+ const delegateId = await resolveUser(opts.to, credentials, client);
1662
+ await client.updateIssue(issueId, { delegateId });
1663
+ const format = getFormat(globalOpts.format);
1664
+ printResult(
1665
+ { data: { status: "delegated", issueId, delegateId } },
1666
+ format
1667
+ );
1668
+ });
1669
+ });
1670
+ delegate.command("list").description("List issues delegated to this agent").action(async (_opts, cmd) => {
1671
+ const globalOpts = cmd.optsWithGlobals();
1672
+ await runWithClient(globalOpts, async (client, { credentials }) => {
1673
+ const result = await client.issues({
1674
+ filter: { delegate: { id: { eq: credentials.actorId } } }
1675
+ });
1676
+ const issueData = await Promise.all(
1677
+ result.nodes.map(async (issue) => {
1678
+ const state = await issue.state;
1679
+ const assignee = await issue.assignee;
1680
+ return {
1681
+ id: issue.identifier,
1682
+ title: issue.title,
1683
+ state: state?.name ?? null,
1684
+ assignee: assignee?.name ?? null,
1685
+ priority: issue.priority
1686
+ };
1687
+ })
1688
+ );
1689
+ const format = getFormat(globalOpts.format);
1690
+ printResult({ data: issueData }, format);
1691
+ });
1692
+ });
1693
+ delegate.command("remove").description("Remove delegation from an issue").argument("<issue-id>", "Issue identifier (e.g., TEAM-123)").action(async (issueId, _opts, cmd) => {
1694
+ const globalOpts = cmd.optsWithGlobals();
1695
+ await runWithClient(globalOpts, async (client) => {
1696
+ await client.updateIssue(issueId, { delegateId: null });
1697
+ const format = getFormat(globalOpts.format);
1698
+ printResult(
1699
+ { data: { status: "delegation_removed", issueId } },
1700
+ format
1701
+ );
1702
+ });
1703
+ });
1704
+ }
1705
+
1706
+ // src/commands/label.ts
1707
+ function registerLabelCommands(program2) {
1708
+ const label = program2.command("label").description("Manage labels");
1709
+ label.command("list").description("List all labels in the workspace").option("--team <team>", "Filter by team name or ID").action(async (opts, cmd) => {
1710
+ const globalOpts = cmd.optsWithGlobals();
1711
+ await runWithClient(globalOpts, async (client) => {
1712
+ const filter = {};
1713
+ if (opts.team) {
1714
+ filter.team = { name: { eqIgnoreCase: opts.team } };
1715
+ }
1716
+ const result = await client.issueLabels({
1717
+ filter: Object.keys(filter).length > 0 ? filter : void 0
1718
+ });
1719
+ const labels = await Promise.all(
1720
+ result.nodes.map(async (l) => {
1721
+ const team = await l.team;
1722
+ return {
1723
+ id: l.id,
1724
+ name: l.name,
1725
+ color: l.color,
1726
+ team: team?.name ?? null
1727
+ };
1728
+ })
1729
+ );
1730
+ const format = getFormat(globalOpts.format);
1731
+ printResult({ data: labels }, format);
1732
+ });
1733
+ });
1734
+ label.command("create").description("Create a new label").requiredOption("--name <text>", "Label name").option("--color <hex>", "Label color as hex").option("--team <team>", "Team for team-scoped label").action(async (opts, cmd) => {
1735
+ const globalOpts = cmd.optsWithGlobals();
1736
+ await runWithClient(globalOpts, async (client) => {
1737
+ const input = { name: opts.name };
1738
+ if (opts.color) input.color = opts.color;
1739
+ if (opts.team) {
1740
+ const teams = await client.teams({
1741
+ filter: { name: { eqIgnoreCase: opts.team } }
1742
+ });
1743
+ const team = teams.nodes[0];
1744
+ if (!team) {
1745
+ console.error(`Error: Team "${opts.team}" not found`);
1746
+ process.exit(4);
1747
+ }
1748
+ input.teamId = team.id;
1749
+ }
1750
+ const result = await client.createIssueLabel(input);
1751
+ const created = await result.issueLabel;
1752
+ const format = getFormat(globalOpts.format);
1753
+ printResult(
1754
+ {
1755
+ data: {
1756
+ id: created?.id,
1757
+ name: created?.name,
1758
+ color: created?.color
1759
+ }
1760
+ },
1761
+ format
1762
+ );
1763
+ });
1764
+ });
1765
+ }
1766
+
1767
+ // src/commands/user.ts
1768
+ function registerUserCommands(program2) {
1769
+ const user = program2.command("user").description("User and agent discovery");
1770
+ user.command("list").description("List all users and agents in the workspace").option("--type <type>", "Filter by entity type (user, app, bot)").option("--team <team>", "Filter by team membership").action(async (opts, cmd) => {
1771
+ const globalOpts = cmd.optsWithGlobals();
1772
+ await runWithClient(globalOpts, async (client) => {
1773
+ let users;
1774
+ if (opts.team) {
1775
+ const teams = await client.teams({
1776
+ filter: { name: { eqIgnoreCase: opts.team } }
1777
+ });
1778
+ const team = teams.nodes[0];
1779
+ if (!team) {
1780
+ console.error(`Error: Team "${opts.team}" not found`);
1781
+ process.exit(4);
1782
+ }
1783
+ const members = await team.members();
1784
+ users = members.nodes;
1785
+ } else {
1786
+ const result = await client.users();
1787
+ users = result.nodes;
1788
+ }
1789
+ let userData = users.map((u) => ({
1790
+ id: u.id,
1791
+ name: u.name,
1792
+ displayName: u.displayName,
1793
+ email: u.email ?? null,
1794
+ isMe: u.isMe ?? null,
1795
+ active: u.active
1796
+ }));
1797
+ if (opts.type) {
1798
+ const type = opts.type.toLowerCase();
1799
+ userData = userData.filter((u) => {
1800
+ if (type === "app" || type === "bot") return !u.email;
1801
+ if (type === "user") return !!u.email;
1802
+ return true;
1803
+ });
1804
+ }
1805
+ const format = getFormat(globalOpts.format);
1806
+ printResult({ data: userData }, format);
1807
+ });
1808
+ });
1809
+ user.command("search").description("Search users/agents by name or email").argument("<query>", "Search query").action(async (query, _opts, cmd) => {
1810
+ const globalOpts = cmd.optsWithGlobals();
1811
+ await runWithClient(globalOpts, async (client) => {
1812
+ const result = await client.users();
1813
+ const queryLower = query.toLowerCase();
1814
+ const matches = result.nodes.filter(
1815
+ (u) => u.name?.toLowerCase().includes(queryLower) || u.email?.toLowerCase().includes(queryLower) || u.displayName?.toLowerCase().includes(queryLower)
1816
+ );
1817
+ const userData = matches.map((u) => ({
1818
+ id: u.id,
1819
+ name: u.name,
1820
+ displayName: u.displayName,
1821
+ email: u.email ?? null
1822
+ }));
1823
+ const format = getFormat(globalOpts.format);
1824
+ printResult({ data: userData }, format);
1825
+ });
1826
+ });
1827
+ user.command("me").description("Show this agent's identity").action(async (_opts, cmd) => {
1828
+ const globalOpts = cmd.optsWithGlobals();
1829
+ const agent = globalOpts.agent;
1830
+ if (!agent) {
1831
+ console.error("Error: --agent is required (or set LINEAR_AGENT_ID env var)");
1832
+ process.exit(4);
1833
+ }
1834
+ const credentialsDir = getCredentialsDir(globalOpts);
1835
+ const credentials = readCredentials(agent, credentialsDir);
1836
+ const format = getFormat(globalOpts.format);
1837
+ printResult(
1838
+ {
1839
+ data: {
1840
+ agent,
1841
+ actorId: credentials.actorId,
1842
+ workspace: credentials.workspaceSlug,
1843
+ authMethod: credentials.authMethod,
1844
+ tokenExpiresAt: credentials.tokenExpiresAt
1845
+ }
1846
+ },
1847
+ format
1848
+ );
1849
+ });
1850
+ }
1851
+
1852
+ // src/commands/team.ts
1853
+ function registerTeamCommands(program2) {
1854
+ const team = program2.command("team").description("Team queries");
1855
+ team.command("list").description("List all teams").action(async (_opts, cmd) => {
1856
+ const globalOpts = cmd.optsWithGlobals();
1857
+ await runWithClient(globalOpts, async (client) => {
1858
+ const result = await client.teams();
1859
+ const teams = result.nodes.map((t) => ({
1860
+ id: t.id,
1861
+ key: t.key,
1862
+ name: t.name,
1863
+ description: t.description ?? null
1864
+ }));
1865
+ const format = getFormat(globalOpts.format);
1866
+ printResult({ data: teams }, format);
1867
+ });
1868
+ });
1869
+ team.command("members").description("List members of a team").argument("<team>", "Team name or key").action(async (teamName, _opts, cmd) => {
1870
+ const globalOpts = cmd.optsWithGlobals();
1871
+ await runWithClient(globalOpts, async (client) => {
1872
+ const teams = await client.teams({
1873
+ filter: { name: { eqIgnoreCase: teamName } }
1874
+ });
1875
+ let team2 = teams.nodes[0];
1876
+ if (!team2) {
1877
+ const byKey = await client.teams({
1878
+ filter: { key: { eq: teamName.toUpperCase() } }
1879
+ });
1880
+ team2 = byKey.nodes[0];
1881
+ }
1882
+ if (!team2) {
1883
+ console.error(`Error: Team "${teamName}" not found`);
1884
+ process.exit(4);
1885
+ }
1886
+ const members = await team2.members();
1887
+ const memberData = members.nodes.map((m) => ({
1888
+ id: m.id,
1889
+ name: m.name,
1890
+ displayName: m.displayName,
1891
+ email: m.email ?? null,
1892
+ active: m.active
1893
+ }));
1894
+ const format = getFormat(globalOpts.format);
1895
+ printResult({ data: memberData }, format);
1896
+ });
1897
+ });
1898
+ }
1899
+
1900
+ // src/commands/project.ts
1901
+ var import_fs5 = require("fs");
1902
+ async function resolveTeamId(client, team) {
1903
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(team)) {
1904
+ return team;
1905
+ }
1906
+ const teams = await client.teams({
1907
+ filter: { name: { eqIgnoreCase: team } }
1908
+ });
1909
+ const match = teams.nodes[0];
1910
+ if (!match) {
1911
+ console.error(`Error: Team "${team}" not found`);
1912
+ process.exit(4);
1913
+ }
1914
+ return match.id;
1915
+ }
1916
+ function registerProjectCommands(program2) {
1917
+ const project = program2.command("project").description("Project queries");
1918
+ project.command("create").description("Create a new project").requiredOption("--name <text>", "Project name").requiredOption("--team <team>", "Associate project with team (name or ID)").option("--description <text>", "Project description (markdown, 255-char limit)").option("--description-file <path>", "Read description from file").option("--content <text>", "Project overview content (long-form markdown)").option("--content-file <path>", "Read project overview content from file").option("--start-date <date>", "Start date (YYYY-MM-DD)").option("--target-date <date>", "Target date (YYYY-MM-DD)").option("--lead <user>", "Project lead (name or email)").option("--priority <n>", "Priority (0=none, 1=urgent, 2=high, 3=normal, 4=low)").action(async (opts, cmd) => {
1919
+ const globalOpts = cmd.optsWithGlobals();
1920
+ await runWithClient(globalOpts, async (client, { credentials }) => {
1921
+ const input = {
1922
+ name: opts.name,
1923
+ teamIds: [await resolveTeamId(client, opts.team)]
1924
+ };
1925
+ if (opts.descriptionFile) {
1926
+ input.description = (0, import_fs5.readFileSync)(opts.descriptionFile, "utf-8");
1927
+ } else if (opts.description) {
1928
+ input.description = opts.description;
1929
+ }
1930
+ if (opts.contentFile) {
1931
+ input.content = (0, import_fs5.readFileSync)(opts.contentFile, "utf-8");
1932
+ } else if (opts.content) {
1933
+ input.content = opts.content;
1934
+ }
1935
+ if (opts.startDate) {
1936
+ input.startDate = opts.startDate;
1937
+ }
1938
+ if (opts.targetDate) {
1939
+ input.targetDate = opts.targetDate;
1940
+ }
1941
+ if (opts.lead) {
1942
+ input.leadId = await resolveUser(opts.lead, credentials, client);
1943
+ }
1944
+ if (opts.priority !== void 0) {
1945
+ const priority = parseInt(opts.priority, 10);
1946
+ if (isNaN(priority) || priority < 0 || priority > 4) {
1947
+ console.error(`Invalid value for --priority: "${opts.priority}". Expected an integer between 0 and 4.`);
1948
+ process.exit(1);
1949
+ }
1950
+ input.priority = priority;
1951
+ }
1952
+ const payload = await client.createProject(input);
1953
+ const created = await payload.project;
1954
+ const format = getFormat(globalOpts.format);
1955
+ printResult(
1956
+ {
1957
+ data: {
1958
+ id: created?.id ?? null,
1959
+ name: created?.name ?? opts.name,
1960
+ url: created?.url ?? null,
1961
+ success: payload.success
1962
+ }
1963
+ },
1964
+ format
1965
+ );
1966
+ });
1967
+ });
1968
+ project.command("list").description("List projects").option("--team <team>", "Filter by team").action(async (opts, cmd) => {
1969
+ const globalOpts = cmd.optsWithGlobals();
1970
+ await runWithClient(globalOpts, async (client) => {
1971
+ const filter = {};
1972
+ if (opts.team) {
1973
+ filter.accessibleTeams = { name: { eqIgnoreCase: opts.team } };
1974
+ }
1975
+ const result = await client.projects({
1976
+ filter: Object.keys(filter).length > 0 ? filter : void 0
1977
+ });
1978
+ const projects = result.nodes.map((p) => ({
1979
+ id: p.id,
1980
+ name: p.name,
1981
+ state: p.state,
1982
+ progress: p.progress,
1983
+ startDate: p.startDate ?? null,
1984
+ targetDate: p.targetDate ?? null
1985
+ }));
1986
+ const format = getFormat(globalOpts.format);
1987
+ printResult({ data: projects }, format);
1988
+ });
1989
+ });
1990
+ project.command("get").description("Get project details").argument("<id>", "Project ID").action(async (id, _opts, cmd) => {
1991
+ const globalOpts = cmd.optsWithGlobals();
1992
+ await runWithClient(globalOpts, async (client) => {
1993
+ const p = await client.project(id);
1994
+ const format = getFormat(globalOpts.format);
1995
+ printResult(
1996
+ {
1997
+ data: {
1998
+ id: p.id,
1999
+ name: p.name,
2000
+ description: p.description ?? null,
2001
+ content: p.content ?? null,
2002
+ state: p.state,
2003
+ progress: p.progress,
2004
+ startDate: p.startDate ?? null,
2005
+ targetDate: p.targetDate ?? null
2006
+ }
2007
+ },
2008
+ format
2009
+ );
2010
+ });
2011
+ });
2012
+ project.command("update").description("Update project metadata").argument("<id>", "Project ID").option("--name <text>", "New project name").option("--description <text>", "Project description (markdown, 255-char limit)").option("--description-file <path>", "Read description from file").option("--content <text>", "Project overview content (long-form markdown)").option("--content-file <path>", "Read project overview content from file").option("--start-date <date>", 'Start date (YYYY-MM-DD, or "null" to clear)').option("--target-date <date>", 'Target date (YYYY-MM-DD, or "null" to clear)').option("--lead <user>", 'Project lead (name, email, or "null" to clear)').option("--priority <n>", "Priority (0=none, 1=urgent, 2=high, 3=normal, 4=low)").action(async (id, opts, cmd) => {
2013
+ const globalOpts = cmd.optsWithGlobals();
2014
+ await runWithClient(globalOpts, async (client, { credentials }) => {
2015
+ const input = {};
2016
+ if (opts.name) {
2017
+ input.name = opts.name;
2018
+ }
2019
+ if (opts.descriptionFile) {
2020
+ input.description = (0, import_fs5.readFileSync)(opts.descriptionFile, "utf-8");
2021
+ } else if (opts.description) {
2022
+ input.description = opts.description;
2023
+ }
2024
+ if (opts.contentFile) {
2025
+ input.content = (0, import_fs5.readFileSync)(opts.contentFile, "utf-8");
2026
+ } else if (opts.content) {
2027
+ input.content = opts.content;
2028
+ }
2029
+ if (opts.startDate !== void 0) {
2030
+ input.startDate = opts.startDate === "null" ? null : opts.startDate;
2031
+ }
2032
+ if (opts.targetDate !== void 0) {
2033
+ input.targetDate = opts.targetDate === "null" ? null : opts.targetDate;
2034
+ }
2035
+ if (opts.lead !== void 0) {
2036
+ if (opts.lead === "null") {
2037
+ input.leadId = null;
2038
+ } else {
2039
+ input.leadId = await resolveUser(opts.lead, credentials, client);
2040
+ }
2041
+ }
2042
+ if (opts.priority !== void 0) {
2043
+ const priority = parseInt(opts.priority, 10);
2044
+ if (isNaN(priority) || priority < 0 || priority > 4) {
2045
+ console.error(`Invalid value for --priority: "${opts.priority}". Expected an integer between 0 and 4.`);
2046
+ process.exit(1);
2047
+ }
2048
+ input.priority = priority;
2049
+ }
2050
+ const payload = await client.updateProject(id, input);
2051
+ const updated = await payload.project;
2052
+ const format = getFormat(globalOpts.format);
2053
+ printResult(
2054
+ {
2055
+ data: {
2056
+ id: updated?.id ?? id,
2057
+ name: updated?.name ?? null,
2058
+ url: updated?.url ?? null,
2059
+ success: payload.success
2060
+ }
2061
+ },
2062
+ format
2063
+ );
2064
+ });
2065
+ });
2066
+ }
2067
+
2068
+ // src/commands/attachment.ts
2069
+ var import_fs6 = require("fs");
2070
+ var import_path3 = require("path");
2071
+ var CONTENT_TYPES = {
2072
+ ".png": "image/png",
2073
+ ".jpg": "image/jpeg",
2074
+ ".jpeg": "image/jpeg",
2075
+ ".gif": "image/gif",
2076
+ ".webp": "image/webp",
2077
+ ".svg": "image/svg+xml",
2078
+ ".pdf": "application/pdf",
2079
+ ".txt": "text/plain",
2080
+ ".md": "text/markdown",
2081
+ ".json": "application/json",
2082
+ ".csv": "text/csv",
2083
+ ".zip": "application/zip"
2084
+ };
2085
+ function registerAttachmentCommands(program2) {
2086
+ const attachment = program2.command("attachment").description("Manage issue attachments");
2087
+ attachment.command("add").description("Add an attachment to an issue (idempotent per URL)").argument("<issue-id>", "Issue identifier (e.g., TEAM-123)").requiredOption("--url <url>", "URL to attach").option("--title <text>", "Display title for the link").action(async (issueId, opts, cmd) => {
2088
+ const globalOpts = cmd.optsWithGlobals();
2089
+ await runWithClient(globalOpts, async (client) => {
2090
+ const input = {
2091
+ issueId,
2092
+ url: opts.url
2093
+ };
2094
+ if (opts.title) input.title = opts.title;
2095
+ const result = await client.createAttachment(input);
2096
+ const created = await result.attachment;
2097
+ const format = getFormat(globalOpts.format);
2098
+ printResult(
2099
+ {
2100
+ data: {
2101
+ id: created?.id,
2102
+ url: opts.url,
2103
+ title: opts.title ?? null,
2104
+ issueId
2105
+ }
2106
+ },
2107
+ format
2108
+ );
2109
+ });
2110
+ });
2111
+ attachment.command("list").description("List attachments on an issue").argument("<issue-id>", "Issue identifier (e.g., TEAM-123)").action(async (issueId, _opts, cmd) => {
2112
+ const globalOpts = cmd.optsWithGlobals();
2113
+ await runWithClient(globalOpts, async (client) => {
2114
+ const issue = await client.issue(issueId);
2115
+ const attachments = await issue.attachments();
2116
+ const attachmentData = attachments.nodes.map((a) => ({
2117
+ id: a.id,
2118
+ url: a.url,
2119
+ title: a.title ?? null
2120
+ }));
2121
+ const format = getFormat(globalOpts.format);
2122
+ printResult({ data: attachmentData }, format);
2123
+ });
2124
+ });
2125
+ attachment.command("remove").description("Remove an attachment").argument("<attachment-id>", "Attachment ID").action(async (attachmentId, _opts, cmd) => {
2126
+ const globalOpts = cmd.optsWithGlobals();
2127
+ await runWithClient(globalOpts, async (client) => {
2128
+ await client.deleteAttachment(attachmentId);
2129
+ const format = getFormat(globalOpts.format);
2130
+ printResult(
2131
+ { data: { status: "removed", attachmentId } },
2132
+ format
2133
+ );
2134
+ });
2135
+ });
2136
+ attachment.command("upload").description("Upload a local file and attach it to an issue or project").argument("<file-path>", "Path to the local file to upload").option("--issue <id>", "Issue identifier to attach the file to (e.g., TEAM-123)").option("--project <id>", "Project name or ID to upload the file for").option("--title <text>", "Display title for the attachment (defaults to filename)").action(async (filePath, opts, cmd) => {
2137
+ const globalOpts = cmd.optsWithGlobals();
2138
+ await runWithClient(globalOpts, async (client) => {
2139
+ if (!opts.issue && !opts.project) {
2140
+ console.error("Error: --issue or --project is required");
2141
+ process.exit(4);
2142
+ }
2143
+ const resolvedPath = (0, import_path3.resolve)(filePath);
2144
+ let stat;
2145
+ try {
2146
+ stat = (0, import_fs6.statSync)(resolvedPath);
2147
+ } catch {
2148
+ console.error(`Error: file not found: ${filePath}`);
2149
+ process.exit(4);
2150
+ }
2151
+ if (!stat.isFile()) {
2152
+ console.error(`Error: ${filePath} is not a file`);
2153
+ process.exit(4);
2154
+ }
2155
+ const filename = (0, import_path3.basename)(resolvedPath);
2156
+ const ext = (0, import_path3.extname)(filename).toLowerCase();
2157
+ const contentType = CONTENT_TYPES[ext] ?? "application/octet-stream";
2158
+ const size = stat.size;
2159
+ const uploadPayload = await client.fileUpload(contentType, filename, size);
2160
+ const uploadFile = uploadPayload.uploadFile;
2161
+ if (!uploadFile) {
2162
+ throw new Error("Failed to get upload URL from Linear");
2163
+ }
2164
+ let fileContent;
2165
+ try {
2166
+ fileContent = (0, import_fs6.readFileSync)(resolvedPath);
2167
+ } catch {
2168
+ console.error(`Error: could not read file: ${filePath}`);
2169
+ process.exit(4);
2170
+ }
2171
+ const headers = {
2172
+ "Content-Type": contentType,
2173
+ "Cache-Control": "public, max-age=31536000"
2174
+ };
2175
+ for (const h of uploadFile.headers) {
2176
+ headers[h.key] = h.value;
2177
+ }
2178
+ const uploadResponse = await fetch(uploadFile.uploadUrl, {
2179
+ method: "PUT",
2180
+ headers,
2181
+ body: fileContent
2182
+ });
2183
+ if (!uploadResponse.ok) {
2184
+ throw new Error(
2185
+ `Upload failed: ${uploadResponse.status} ${uploadResponse.statusText}`
2186
+ );
2187
+ }
2188
+ const title = opts.title ?? filename;
2189
+ const format = getFormat(globalOpts.format);
2190
+ if (opts.issue) {
2191
+ const result = await client.createAttachment({
2192
+ issueId: opts.issue,
2193
+ url: uploadFile.assetUrl,
2194
+ title
2195
+ });
2196
+ const created = await result.attachment;
2197
+ printResult(
2198
+ {
2199
+ data: {
2200
+ id: created?.id,
2201
+ url: uploadFile.assetUrl,
2202
+ title,
2203
+ issueId: opts.issue
2204
+ }
2205
+ },
2206
+ format
2207
+ );
2208
+ } else {
2209
+ printResult(
2210
+ {
2211
+ data: {
2212
+ url: uploadFile.assetUrl,
2213
+ title,
2214
+ projectId: opts.project
2215
+ }
2216
+ },
2217
+ format
2218
+ );
2219
+ }
2220
+ });
2221
+ });
2222
+ }
2223
+
2224
+ // src/commands/state.ts
2225
+ function registerStateCommands(program2) {
2226
+ const state = program2.command("state").description("Workflow state queries");
2227
+ state.command("list").description("List workflow states").option("--team <team>", "Filter by team name or key").action(async (opts, cmd) => {
2228
+ const globalOpts = cmd.optsWithGlobals();
2229
+ await runWithClient(globalOpts, async (client, { agentId, credentialsDir }) => {
2230
+ let cache = readCache(agentId, credentialsDir);
2231
+ if (opts.team) {
2232
+ const teams = await client.teams({
2233
+ filter: { name: { eqIgnoreCase: opts.team } }
2234
+ });
2235
+ let team = teams.nodes[0];
2236
+ if (!team) {
2237
+ const byKey = await client.teams({
2238
+ filter: { key: { eq: opts.team.toUpperCase() } }
2239
+ });
2240
+ team = byKey.nodes[0];
2241
+ }
2242
+ if (!team) {
2243
+ console.error(`Error: Team "${opts.team}" not found`);
2244
+ process.exit(4);
2245
+ }
2246
+ const states = await team.states();
2247
+ const stateData = states.nodes.map((s) => ({
2248
+ id: s.id,
2249
+ name: s.name,
2250
+ type: s.type,
2251
+ color: s.color,
2252
+ position: s.position,
2253
+ team: team.name
2254
+ }));
2255
+ const stateMap = {};
2256
+ for (const s of states.nodes) {
2257
+ stateMap[s.name] = s.id;
2258
+ }
2259
+ cache = setTeamStates(cache, team.key, stateMap);
2260
+ writeCache(agentId, credentialsDir, cache);
2261
+ const format = getFormat(globalOpts.format);
2262
+ printResult({ data: stateData }, format);
2263
+ } else {
2264
+ const teamsResult = await client.teams();
2265
+ const allStates = [];
2266
+ for (const t of teamsResult.nodes) {
2267
+ const states = await t.states();
2268
+ const stateMap = {};
2269
+ for (const s of states.nodes) {
2270
+ stateMap[s.name] = s.id;
2271
+ allStates.push({
2272
+ id: s.id,
2273
+ name: s.name,
2274
+ type: s.type,
2275
+ color: s.color,
2276
+ position: s.position,
2277
+ team: t.name
2278
+ });
2279
+ }
2280
+ cache = setTeamStates(cache, t.key, stateMap);
2281
+ }
2282
+ writeCache(agentId, credentialsDir, cache);
2283
+ const format = getFormat(globalOpts.format);
2284
+ printResult({ data: allStates }, format);
2285
+ }
2286
+ });
2287
+ });
2288
+ }
2289
+
2290
+ // src/cli.ts
2291
+ var pkg = JSON.parse(
2292
+ (0, import_fs7.readFileSync)((0, import_path4.join)(__dirname, "..", "package.json"), "utf-8")
2293
+ );
2294
+ var program = new import_commander.Command();
2295
+ program.name("linear").description("CLI tool for AI agents to interact with Linear").version(pkg.version).option("--agent <id>", "agent identifier (env: LINEAR_AGENT_ID)", process.env.LINEAR_AGENT_ID).option(
2296
+ "--credentials-dir <path>",
2297
+ "path to credentials directory (env: LINEAR_AGENT_CREDENTIALS_DIR)",
2298
+ process.env.LINEAR_AGENT_CREDENTIALS_DIR ?? "~/.linear/credentials/"
2299
+ ).addOption(
2300
+ new import_commander.Option("--format <format>", "output format (default: auto-detect TTY)").choices(["json", "text"])
2301
+ );
2302
+ registerAuthCommands(program);
2303
+ registerIssueCommands(program);
2304
+ registerCommentCommands(program);
2305
+ registerInboxCommands(program);
2306
+ registerDelegateCommands(program);
2307
+ registerLabelCommands(program);
2308
+ registerUserCommands(program);
2309
+ registerTeamCommands(program);
2310
+ registerProjectCommands(program);
2311
+ registerAttachmentCommands(program);
2312
+ registerStateCommands(program);
2313
+ program.parseAsync(process.argv).catch((err) => {
2314
+ if (err instanceof CLIError) {
2315
+ console.error(`Error: ${err.message}`);
2316
+ if (err.resolution) console.error(err.resolution);
2317
+ process.exit(err.exitCode);
2318
+ }
2319
+ console.error(`Error: ${err?.message ?? String(err)}`);
2320
+ process.exit(1);
2321
+ });