@enrichlayer/el-linear 1.9.0 → 1.15.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 (135) hide show
  1. package/README.md +139 -10
  2. package/claude-skills/linear-operations/SKILL.md +41 -1
  3. package/dist/auth/linear-credential.d.ts +27 -0
  4. package/dist/auth/linear-credential.js +1 -0
  5. package/dist/auth/oauth-app-config.d.ts +4 -3
  6. package/dist/auth/oauth-app-config.js +13 -2
  7. package/dist/auth/oauth-callback.d.ts +2 -3
  8. package/dist/auth/oauth-callback.js +2 -2
  9. package/dist/auth/oauth-client.d.ts +8 -2
  10. package/dist/auth/oauth-client.js +26 -0
  11. package/dist/auth/oauth-fs.d.ts +2 -1
  12. package/dist/auth/oauth-headless.d.ts +2 -1
  13. package/dist/auth/oauth-storage.d.ts +5 -1
  14. package/dist/auth/oauth-storage.js +1 -1
  15. package/dist/auth/oauth-token.d.ts +4 -3
  16. package/dist/auth/oauth-token.js +16 -4
  17. package/dist/auth/token-resolver.d.ts +14 -5
  18. package/dist/auth/token-resolver.js +6 -1
  19. package/dist/commands/attachments.js +2 -1
  20. package/dist/commands/batch.js +18 -21
  21. package/dist/commands/comments.js +22 -33
  22. package/dist/commands/config.js +178 -5
  23. package/dist/commands/cycles.js +2 -1
  24. package/dist/commands/documents.js +2 -1
  25. package/dist/commands/graphql.js +4 -6
  26. package/dist/commands/init/aliases.js +1 -1
  27. package/dist/commands/init/defaults.d.ts +2 -1
  28. package/dist/commands/init/index.js +45 -35
  29. package/dist/commands/init/oauth.d.ts +4 -1
  30. package/dist/commands/init/oauth.js +22 -4
  31. package/dist/commands/init/shared.d.ts +24 -2
  32. package/dist/commands/init/shared.js +35 -4
  33. package/dist/commands/init/token.d.ts +3 -3
  34. package/dist/commands/init/token.js +5 -24
  35. package/dist/commands/init/workspace.d.ts +2 -1
  36. package/dist/commands/init/workspace.js +1 -1
  37. package/dist/commands/introspect.d.ts +27 -0
  38. package/dist/commands/introspect.js +178 -0
  39. package/dist/commands/issue-id.js +1 -3
  40. package/dist/commands/issues/branch.js +9 -1
  41. package/dist/commands/issues/description.js +2 -6
  42. package/dist/commands/issues/link-references.d.ts +21 -0
  43. package/dist/commands/issues/link-references.js +171 -0
  44. package/dist/commands/issues/relations.d.ts +44 -0
  45. package/dist/commands/issues/relations.js +132 -0
  46. package/dist/commands/issues.js +269 -309
  47. package/dist/commands/labels.js +15 -24
  48. package/dist/commands/profile.js +1 -0
  49. package/dist/commands/project-milestones.js +13 -20
  50. package/dist/commands/projects.d.ts +2 -0
  51. package/dist/commands/projects.js +157 -44
  52. package/dist/commands/read-shortcut.d.ts +1 -1
  53. package/dist/commands/read-shortcut.js +28 -8
  54. package/dist/commands/refs.js +75 -8
  55. package/dist/commands/releases.js +26 -30
  56. package/dist/commands/search.js +49 -33
  57. package/dist/commands/teams.js +2 -1
  58. package/dist/commands/templates.js +9 -14
  59. package/dist/commands/users.js +5 -2
  60. package/dist/config/config.d.ts +99 -1
  61. package/dist/config/config.js +264 -52
  62. package/dist/config/error-enrichment.d.ts +62 -0
  63. package/dist/config/error-enrichment.js +417 -0
  64. package/dist/config/issue-validation.d.ts +37 -0
  65. package/dist/config/issue-validation.js +63 -1
  66. package/dist/config/paths.d.ts +2 -8
  67. package/dist/config/paths.js +4 -2
  68. package/dist/config/resolver.d.ts +8 -1
  69. package/dist/config/resolver.js +11 -5
  70. package/dist/main.js +13 -1
  71. package/dist/queries/attachments-types.d.ts +30 -0
  72. package/dist/queries/attachments-types.js +5 -0
  73. package/dist/queries/comments-types.d.ts +55 -0
  74. package/dist/queries/comments-types.js +5 -0
  75. package/dist/queries/common.d.ts +2 -2
  76. package/dist/queries/common.js +8 -0
  77. package/dist/queries/documents-types.d.ts +62 -0
  78. package/dist/queries/documents-types.js +9 -0
  79. package/dist/queries/introspect-types.d.ts +58 -0
  80. package/dist/queries/introspect-types.js +10 -0
  81. package/dist/queries/issues-types.d.ts +481 -0
  82. package/dist/queries/issues-types.js +23 -0
  83. package/dist/queries/issues.d.ts +51 -10
  84. package/dist/queries/issues.js +147 -5
  85. package/dist/queries/labels-types.d.ts +65 -0
  86. package/dist/queries/labels-types.js +5 -0
  87. package/dist/queries/project-milestones-types.d.ts +92 -0
  88. package/dist/queries/project-milestones-types.js +10 -0
  89. package/dist/queries/project-milestones.d.ts +1 -1
  90. package/dist/queries/projects-types.d.ts +76 -0
  91. package/dist/queries/projects-types.js +5 -0
  92. package/dist/queries/projects.d.ts +2 -0
  93. package/dist/queries/projects.js +22 -0
  94. package/dist/queries/releases-types.d.ts +85 -0
  95. package/dist/queries/releases-types.js +5 -0
  96. package/dist/queries/search-types.d.ts +102 -0
  97. package/dist/queries/search-types.js +6 -0
  98. package/dist/queries/templates-types.d.ts +62 -0
  99. package/dist/queries/templates-types.js +9 -0
  100. package/dist/types/linear.d.ts +21 -3
  101. package/dist/utils/auto-link-references.d.ts +3 -3
  102. package/dist/utils/auto-link-references.js +30 -34
  103. package/dist/utils/extract-field.d.ts +19 -0
  104. package/dist/utils/extract-field.js +99 -0
  105. package/dist/utils/file-service.d.ts +6 -13
  106. package/dist/utils/file-service.js +0 -2
  107. package/dist/utils/formatters/summary.js +6 -1
  108. package/dist/utils/graphql-attachments-service.js +6 -9
  109. package/dist/utils/graphql-documents-service.js +19 -25
  110. package/dist/utils/graphql-issues-service.d.ts +112 -46
  111. package/dist/utils/graphql-issues-service.js +398 -206
  112. package/dist/utils/graphql-service.d.ts +10 -12
  113. package/dist/utils/graphql-service.js +0 -3
  114. package/dist/utils/issue-reference-extractor.d.ts +7 -0
  115. package/dist/utils/issue-reference-extractor.js +5 -3
  116. package/dist/utils/issues-service-bootstrap.d.ts +28 -0
  117. package/dist/utils/issues-service-bootstrap.js +27 -0
  118. package/dist/utils/linear-service.d.ts +21 -14
  119. package/dist/utils/linear-service.js +73 -11
  120. package/dist/utils/markdown-prosemirror.js +12 -12
  121. package/dist/utils/mention-resolver.js +1 -1
  122. package/dist/utils/output.d.ts +82 -2
  123. package/dist/utils/output.js +76 -11
  124. package/dist/utils/project-slug.d.ts +21 -0
  125. package/dist/utils/project-slug.js +45 -0
  126. package/dist/utils/protected-ranges.d.ts +14 -0
  127. package/dist/utils/protected-ranges.js +88 -2
  128. package/dist/utils/sanitize-for-log.d.ts +24 -0
  129. package/dist/utils/sanitize-for-log.js +38 -0
  130. package/dist/utils/table-formatter.js +24 -0
  131. package/dist/utils/validators.d.ts +7 -2
  132. package/dist/utils/validators.js +6 -0
  133. package/dist/utils/workspace-url.d.ts +5 -1
  134. package/dist/utils/workspace-url.js +53 -7
  135. package/package.json +2 -2
@@ -1,20 +1,18 @@
1
+ import type { LinearCredential } from "../auth/linear-credential.js";
1
2
  import type { GraphQLResponseData, GraphQLVariables } from "../types/linear.js";
2
3
  import type { AuthOptions } from "./auth.js";
3
4
  /**
4
- * Constructor arg shapes for `GraphQLService`. Three variants:
5
- * - `string` → personal API token (legacy; sent without `Bearer` prefix).
6
- * - `{apiKey: string}` → personal API token (explicit).
7
- * - `{oauthToken: string}` → OAuth access token (sent as
8
- * `Authorization: Bearer <token>` via the SDK's accessToken option).
5
+ * Constructor arg for `GraphQLService`. Re-exported alias of the shared
6
+ * `LinearCredential` union (`{ apiKey } | { oauthToken }`). Kept as a
7
+ * named local export so call sites that already import
8
+ * `GraphQLServiceAuth` keep compiling — the alias collapses through to
9
+ * the shared shape.
9
10
  *
10
- * The string variant exists because hundreds of call sites and tests pass
11
- * a plain string. We continue to support it indefinitely.
11
+ * The bare-string legacy arm was dropped in DEV-4068 T7. Tests now
12
+ * construct with `{ apiKey: "test-token" }` (mechanical rewrite, no
13
+ * semantic change — same `Authorization: <token>` header is emitted).
12
14
  */
13
- export type GraphQLServiceAuth = string | {
14
- apiKey: string;
15
- } | {
16
- oauthToken: string;
17
- };
15
+ export type GraphQLServiceAuth = LinearCredential;
18
16
  export declare class GraphQLService {
19
17
  private readonly graphQLClient;
20
18
  constructor(auth: GraphQLServiceAuth);
@@ -2,9 +2,6 @@ import { LinearClient } from "@linear/sdk";
2
2
  import { getActiveAuth } from "../auth/token-resolver.js";
3
3
  function buildLinearClient(auth) {
4
4
  const baseHeaders = { "public-file-urls-expire-in": "3600" };
5
- if (typeof auth === "string") {
6
- return new LinearClient({ apiKey: auth, headers: baseHeaders });
7
- }
8
5
  if ("oauthToken" in auth) {
9
6
  // Linear's SDK natively supports OAuth via the `accessToken` option,
10
7
  // which causes the underlying graphql-request client to send
@@ -5,6 +5,13 @@ export interface IssueReference {
5
5
  reverse: boolean;
6
6
  type: IssueRelationType;
7
7
  }
8
+ /**
9
+ * Specificity ranking — used when the same identifier appears more than once
10
+ * in the text with different qualifiers. The strongest non-default inference
11
+ * wins. Exported because `auto-link-references` does the same merge over its
12
+ * own description+comments fan-in (DEV-4070 deduplication).
13
+ */
14
+ export declare function specificity(ref: IssueReference): number;
8
15
  /**
9
16
  * Extract issue identifiers from text along with the inferred relation type.
10
17
  *
@@ -45,10 +45,12 @@ function inferRelation(textBefore) {
45
45
  return { type: "related", reverse: false };
46
46
  }
47
47
  /**
48
- * Specificity ranking — used when the same identifier appears more than once in the text
49
- * with different qualifiers. The strongest non-default inference wins.
48
+ * Specificity ranking — used when the same identifier appears more than once
49
+ * in the text with different qualifiers. The strongest non-default inference
50
+ * wins. Exported because `auto-link-references` does the same merge over its
51
+ * own description+comments fan-in (DEV-4070 deduplication).
50
52
  */
51
- function specificity(ref) {
53
+ export function specificity(ref) {
52
54
  if (ref.type === "duplicate") {
53
55
  return 3;
54
56
  }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Service-trio bootstrap helper.
3
+ *
4
+ * The 3-line incantation
5
+ *
6
+ * ```ts
7
+ * const graphQLService = await createGraphQLService(rootOpts);
8
+ * const linearService = await createLinearService(rootOpts);
9
+ * const issuesService = new GraphQLIssuesService(graphQLService, linearService);
10
+ * ```
11
+ *
12
+ * repeats verbatim across ~13 issue/batch command handlers. This helper
13
+ * collapses it to a single line; callers destructure only what they use.
14
+ *
15
+ * Sites that need just `graphQLService` + `linearService` (no `issuesService`)
16
+ * can keep their direct calls — there's no benefit to routing through a
17
+ * helper that constructs an unused third service.
18
+ */
19
+ import type { AuthOptions } from "./auth.js";
20
+ import { GraphQLIssuesService } from "./graphql-issues-service.js";
21
+ import { type GraphQLService } from "./graphql-service.js";
22
+ import { type LinearService } from "./linear-service.js";
23
+ export interface IssuesServiceTrio {
24
+ graphQLService: GraphQLService;
25
+ linearService: LinearService;
26
+ issuesService: GraphQLIssuesService;
27
+ }
28
+ export declare function createIssuesService(options: AuthOptions): Promise<IssuesServiceTrio>;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Service-trio bootstrap helper.
3
+ *
4
+ * The 3-line incantation
5
+ *
6
+ * ```ts
7
+ * const graphQLService = await createGraphQLService(rootOpts);
8
+ * const linearService = await createLinearService(rootOpts);
9
+ * const issuesService = new GraphQLIssuesService(graphQLService, linearService);
10
+ * ```
11
+ *
12
+ * repeats verbatim across ~13 issue/batch command handlers. This helper
13
+ * collapses it to a single line; callers destructure only what they use.
14
+ *
15
+ * Sites that need just `graphQLService` + `linearService` (no `issuesService`)
16
+ * can keep their direct calls — there's no benefit to routing through a
17
+ * helper that constructs an unused third service.
18
+ */
19
+ import { GraphQLIssuesService } from "./graphql-issues-service.js";
20
+ import { createGraphQLService, } from "./graphql-service.js";
21
+ import { createLinearService } from "./linear-service.js";
22
+ export async function createIssuesService(options) {
23
+ const graphQLService = await createGraphQLService(options);
24
+ const linearService = await createLinearService(options);
25
+ const issuesService = new GraphQLIssuesService(graphQLService, linearService);
26
+ return { graphQLService, linearService, issuesService };
27
+ }
@@ -1,20 +1,13 @@
1
+ import type { LinearCredential } from "../auth/linear-credential.js";
1
2
  import type { LinearComment, LinearCycleDetail, LinearCycleSummary, LinearLabel, LinearProject, LinearTeam, LinearUser } from "../types/linear.js";
2
3
  import type { AuthOptions } from "./auth.js";
3
4
  /**
4
- * Constructor arg shapes for `LinearService`. Three variants:
5
- * - `string` → personal API token (legacy; sent without `Bearer` prefix).
6
- * - `{apiKey: string}` → personal API token (explicit).
7
- * - `{oauthToken: string}` → OAuth access token (sent as
8
- * `Authorization: Bearer <token>` via the SDK's accessToken option).
9
- *
10
- * The string variant exists because hundreds of call sites and tests pass
11
- * a plain string. We continue to support it indefinitely.
5
+ * Constructor arg for `LinearService`. Re-exported alias of the shared
6
+ * `LinearCredential` union (`{ apiKey } | { oauthToken }`). See
7
+ * `src/auth/linear-credential.ts` for the contract. The bare-string
8
+ * legacy arm was dropped in DEV-4068 T7.
12
9
  */
13
- export type LinearServiceAuth = string | {
14
- apiKey: string;
15
- } | {
16
- oauthToken: string;
17
- };
10
+ export type LinearServiceAuth = LinearCredential;
18
11
  export declare class LinearService {
19
12
  private readonly client;
20
13
  constructor(auth: LinearServiceAuth);
@@ -26,6 +19,8 @@ export declare class LinearService {
26
19
  nameFilter?: string;
27
20
  states?: string[];
28
21
  excludeStates?: string[];
22
+ /** Resolved team UUID — applied as a server-side filter before pagination. */
23
+ teamId?: string;
29
24
  }): Promise<LinearProject[]>;
30
25
  resolveTeamId(teamKeyOrNameOrId: string): Promise<string>;
31
26
  resolveStatusId(statusName: string, teamId?: string): Promise<string>;
@@ -40,6 +35,18 @@ export declare class LinearService {
40
35
  getCycles(teamFilter?: string, activeOnly?: boolean, limit?: number): Promise<LinearCycleSummary[]>;
41
36
  getCycleById(cycleId: string, issuesLimit?: number): Promise<LinearCycleDetail>;
42
37
  resolveCycleId(cycleNameOrId: string, teamFilter?: string): Promise<string>;
43
- resolveProjectId(projectNameOrId: string): Promise<string>;
38
+ resolveProjectId(projectInput: string): Promise<string>;
39
+ /**
40
+ * Normalize a user-supplied project input to a UUID when the input is
41
+ * a URL or slug-id form. Pass-through for UUIDs and plain names — the
42
+ * latter stays a name so downstream batch-resolve queries can fold the
43
+ * lookup into their single round-trip.
44
+ *
45
+ * Used by callers that route project resolution through a separate
46
+ * batch-resolve step (e.g. `GraphqlIssuesService.createIssue`) — they
47
+ * can pre-normalize URL/slug inputs to UUIDs so the batch query's
48
+ * `name eqIgnoreCase` filter doesn't have to learn the URL/slug shape.
49
+ */
50
+ normalizeProjectInput(projectInput: string): Promise<string>;
44
51
  }
45
52
  export declare function createLinearService(options: AuthOptions): Promise<LinearService>;
@@ -4,6 +4,7 @@ import { resolveUserDisplayName } from "../config/resolver.js";
4
4
  import { toISOStringOrNow, toISOStringOrUndefined } from "./date-format.js";
5
5
  import { multipleMatchesError, notFoundError } from "./error-messages.js";
6
6
  import { parseIssueIdentifier } from "./identifier-parser.js";
7
+ import { parseProjectSlugId } from "./project-slug.js";
7
8
  import { isUuid } from "./uuid.js";
8
9
  const DEFAULT_CYCLE_PAGINATION_LIMIT = 250;
9
10
  // The Linear SDK types don't accept string orderBy values, but the API does.
@@ -22,9 +23,6 @@ function nonEmptyFilter(filter) {
22
23
  return Object.keys(filter).length > 0 ? filter : undefined;
23
24
  }
24
25
  function buildLinearClient(auth) {
25
- if (typeof auth === "string") {
26
- return new LinearClient({ apiKey: auth });
27
- }
28
26
  if ("oauthToken" in auth) {
29
27
  // Linear's SDK natively supports OAuth via the `accessToken` option,
30
28
  // which causes the underlying transport to send
@@ -133,13 +131,28 @@ export class LinearService {
133
131
  else if (options.excludeStates && options.excludeStates.length > 0) {
134
132
  filter.state = { nin: options.excludeStates };
135
133
  }
136
- const projects = await this.client.projects({
134
+ if (options.teamId) {
135
+ filter.teams = { some: { id: { eq: options.teamId } } };
136
+ }
137
+ // `limit === 0` means "no limit" — paginate the full result set.
138
+ // Otherwise a single page of `limit` projects. DEV-4175: `--all` /
139
+ // `--limit 0` must return every project so callers never make a false
140
+ // "does not exist" determination off a silently truncated page.
141
+ const unlimited = limit === 0;
142
+ let page = await this.client.projects({
137
143
  filter: nonEmptyFilter(filter),
138
- first: limit,
144
+ first: unlimited ? 250 : limit,
139
145
  orderBy: sdkOrderBy("updatedAt"),
140
146
  includeArchived: false,
141
147
  });
142
- const projectsWithData = await Promise.all(projects.nodes.map(async (project) => {
148
+ const projectNodes = [...page.nodes];
149
+ if (unlimited) {
150
+ while (page.pageInfo.hasNextPage) {
151
+ page = await page.fetchNext();
152
+ projectNodes.push(...page.nodes);
153
+ }
154
+ }
155
+ const projectsWithData = await Promise.all(projectNodes.map(async (project) => {
143
156
  const [teams, lead] = await Promise.all([
144
157
  project.teams(),
145
158
  project.lead,
@@ -373,6 +386,9 @@ export class LinearService {
373
386
  url: issue.url,
374
387
  title: issue.title,
375
388
  description: issue.description || undefined,
389
+ // Linear SDK types `priority` as `number`; the GraphQL schema only
390
+ // emits 0-4, so the cast is safe and the runtime range is
391
+ // guaranteed by Linear's server-side schema.
376
392
  priority: issue.priority,
377
393
  estimate: issue.estimate || undefined,
378
394
  state: state ? { id: state.id, name: state.name } : undefined,
@@ -455,20 +471,66 @@ export class LinearService {
455
471
  }
456
472
  return chosen.id;
457
473
  }
458
- async resolveProjectId(projectNameOrId) {
459
- if (isUuid(projectNameOrId)) {
460
- return projectNameOrId;
474
+ async resolveProjectId(projectInput) {
475
+ if (isUuid(projectInput)) {
476
+ return projectInput;
477
+ }
478
+ const slugId = parseProjectSlugId(projectInput);
479
+ if (slugId) {
480
+ // `as Record<string, unknown>` — `@linear/sdk`'s typed ProjectFilter
481
+ // lags Linear's schema and doesn't yet expose `slugId`. The field is
482
+ // present on the server; the cast is here until the SDK ships the
483
+ // typing. Don't "clean up" without verifying the typed shape exists.
484
+ //
485
+ // `includeArchived: true` — URLs in the wild often point at archived
486
+ // projects (someone digs up an old link); silently 404-ing them is
487
+ // worse than resolving to an archived project, which the caller can
488
+ // inspect via the returned UUID. Name resolution stays active-only
489
+ // because names collide more readily than slug-ids do.
490
+ const bySlug = await this.client.projects({
491
+ filter: { slugId: { eq: slugId } },
492
+ first: 1,
493
+ includeArchived: true,
494
+ });
495
+ if (bySlug.nodes.length > 0) {
496
+ return bySlug.nodes[0].id;
497
+ }
498
+ // Slug-id form was syntactically valid but didn't match a project
499
+ // — fall through to name resolution would be misleading (the user
500
+ // clearly pasted a URL/slug, not a name). Throw the same shape of
501
+ // not-found error as the name path.
502
+ throw notFoundError("Project", projectInput);
461
503
  }
462
- const filter = { name: { eqIgnoreCase: projectNameOrId } };
504
+ const filter = { name: { eqIgnoreCase: projectInput } };
463
505
  const projectsConnection = await this.client.projects({
464
506
  filter,
465
507
  first: 1,
466
508
  });
467
509
  if (projectsConnection.nodes.length === 0) {
468
- throw notFoundError("Project", projectNameOrId);
510
+ throw notFoundError("Project", projectInput);
469
511
  }
470
512
  return projectsConnection.nodes[0].id;
471
513
  }
514
+ /**
515
+ * Normalize a user-supplied project input to a UUID when the input is
516
+ * a URL or slug-id form. Pass-through for UUIDs and plain names — the
517
+ * latter stays a name so downstream batch-resolve queries can fold the
518
+ * lookup into their single round-trip.
519
+ *
520
+ * Used by callers that route project resolution through a separate
521
+ * batch-resolve step (e.g. `GraphqlIssuesService.createIssue`) — they
522
+ * can pre-normalize URL/slug inputs to UUIDs so the batch query's
523
+ * `name eqIgnoreCase` filter doesn't have to learn the URL/slug shape.
524
+ */
525
+ async normalizeProjectInput(projectInput) {
526
+ if (isUuid(projectInput)) {
527
+ return projectInput;
528
+ }
529
+ if (parseProjectSlugId(projectInput)) {
530
+ return this.resolveProjectId(projectInput);
531
+ }
532
+ return projectInput;
533
+ }
472
534
  }
473
535
  export async function createLinearService(options) {
474
536
  const auth = await getActiveAuth(options);
@@ -88,7 +88,7 @@ function parseFencedCodeBlock(state, line) {
88
88
  attrs.language = language;
89
89
  }
90
90
  state.content.push({
91
- type: "codeBlock",
91
+ type: "code_block",
92
92
  ...(Object.keys(attrs).length > 0 ? { attrs } : {}),
93
93
  content: codeLines.length > 0
94
94
  ? [{ type: "text", text: codeLines.join("\n") }]
@@ -100,7 +100,7 @@ function parseHorizontalRule(state, line) {
100
100
  if (!HR_RE.test(line)) {
101
101
  return false;
102
102
  }
103
- state.content.push({ type: "horizontalRule" });
103
+ state.content.push({ type: "horizontal_rule" });
104
104
  state.i++;
105
105
  return true;
106
106
  }
@@ -128,12 +128,12 @@ function parseBulletList(state, line) {
128
128
  break;
129
129
  }
130
130
  items.push({
131
- type: "listItem",
131
+ type: "list_item",
132
132
  content: [{ type: "paragraph", content: parseInline(m[1]) }],
133
133
  });
134
134
  state.i++;
135
135
  }
136
- state.content.push({ type: "bulletList", content: items });
136
+ state.content.push({ type: "bullet_list", content: items });
137
137
  return true;
138
138
  }
139
139
  function parseOrderedList(state, line) {
@@ -147,12 +147,12 @@ function parseOrderedList(state, line) {
147
147
  break;
148
148
  }
149
149
  items.push({
150
- type: "listItem",
150
+ type: "list_item",
151
151
  content: [{ type: "paragraph", content: parseInline(m[1]) }],
152
152
  });
153
153
  state.i++;
154
154
  }
155
- state.content.push({ type: "orderedList", content: items });
155
+ state.content.push({ type: "ordered_list", content: items });
156
156
  return true;
157
157
  }
158
158
  function parseBlockquote(state) {
@@ -217,9 +217,9 @@ function parseTable(state, line) {
217
217
  const headerCells = splitTableRow(rowMatch[1]);
218
218
  const cols = headerCells.length;
219
219
  rows.push({
220
- type: "tableRow",
220
+ type: "table_row",
221
221
  content: headerCells.map((cell) => ({
222
- type: "tableHeader",
222
+ type: "table_header",
223
223
  content: [{ type: "paragraph", content: cellContent(cell) }],
224
224
  })),
225
225
  });
@@ -238,9 +238,9 @@ function parseTable(state, line) {
238
238
  }
239
239
  cells.length = cols;
240
240
  rows.push({
241
- type: "tableRow",
241
+ type: "table_row",
242
242
  content: cells.map((cell) => ({
243
- type: "tableCell",
243
+ type: "table_cell",
244
244
  content: [{ type: "paragraph", content: cellContent(cell) }],
245
245
  })),
246
246
  });
@@ -327,7 +327,7 @@ function findEarliestInlineMatch(text) {
327
327
  index: boldMatch.index ?? 0,
328
328
  length: boldMatch[0].length,
329
329
  innerText: boldMatch[1] ?? boldMatch[2],
330
- marks: [{ type: "bold" }],
330
+ marks: [{ type: "strong" }],
331
331
  });
332
332
  }
333
333
  const italicMatch = text.match(INLINE_ITALIC_RE);
@@ -336,7 +336,7 @@ function findEarliestInlineMatch(text) {
336
336
  index: italicMatch.index ?? 0,
337
337
  length: italicMatch[0].length,
338
338
  innerText: italicMatch[1] ?? italicMatch[2],
339
- marks: [{ type: "italic" }],
339
+ marks: [{ type: "em" }],
340
340
  });
341
341
  }
342
342
  if (candidates.length === 0) {
@@ -146,7 +146,7 @@ function injectMentions(doc, explicit, bare) {
146
146
  return {
147
147
  ...doc,
148
148
  content: doc.content.map((node) => {
149
- if (node.content && node.type !== "codeBlock") {
149
+ if (node.content && node.type !== "code_block") {
150
150
  const processed = injectMentions(node, explicit, bare);
151
151
  if (node.type === "paragraph" || node.type === "heading") {
152
152
  return {
@@ -1,11 +1,91 @@
1
- export type OutputFormat = "json" | "summary";
1
+ type OutputFormat = "json" | "summary";
2
2
  export declare function setRawMode(enabled: boolean): void;
3
3
  export declare function setJqFilter(filter: string | null): void;
4
4
  export declare function setFieldsFilter(fields: string[] | null): void;
5
5
  export declare function setOutputFormat(format: OutputFormat): void;
6
+ /** @internal Test seam — consumers should not depend on the format state. */
6
7
  export declare function getOutputFormat(): OutputFormat;
8
+ /**
9
+ * Resource-specific extra metadata that may be added to a list response
10
+ * alongside the canonical `count` (e.g. `query` on search, `team` on
11
+ * filtered list). Excludes `count` at the type level — callers can't
12
+ * accidentally pass a string `count` and have it silently overridden;
13
+ * the only way to set `count` is via `data.length` inside `outputList`.
14
+ *
15
+ * Implementation note: `count?: never` together with `Record<string, unknown>`
16
+ * lets TypeScript accept any other key while disallowing the literal
17
+ * `count` key. The `never`-typed property is impossible to assign, which
18
+ * is what we want for the "no count here" contract.
19
+ */
20
+ export type ListExtraMeta = Record<string, unknown> & {
21
+ count?: never;
22
+ };
23
+ /**
24
+ * Metadata envelope for list responses. Always includes `count`; resources
25
+ * may add their own keys (e.g. `query` on search, `team` on filtered list).
26
+ *
27
+ * `meta.count` is the cardinality of `data[]` — downstream `jq` pipelines
28
+ * read it both for emptiness checks (`.meta.count == 0`) and for the
29
+ * actual magnitude (e.g. logging "found N issues"). A boolean isEmpty
30
+ * would lose the magnitude signal, so `count: number` stays.
31
+ */
32
+ export interface ListMeta extends Record<string, unknown> {
33
+ count: number;
34
+ }
35
+ /**
36
+ * Canonical JSON envelope for a list response. Pre-DEV-4068 T6, every
37
+ * call site built this object literal inline and `outputSuccess(data:
38
+ * unknown)` accepted it without type-checking. Use `outputList<T>` to
39
+ * route a list through the same emit path with a real per-element type
40
+ * (so e.g. `data: T[]` and `--fields` consumers stay in lock-step).
41
+ */
42
+ export interface CliListEnvelope<T> {
43
+ data: T[];
44
+ meta: ListMeta;
45
+ }
46
+ /**
47
+ * Typed wrapper around `outputSuccess` for list responses — builds the
48
+ * `{ data, meta: { count, ...extraMeta } }` envelope and emits it. The
49
+ * 81 existing inline `outputSuccess({ data, meta: { count, ... } })`
50
+ * call sites can migrate to this incrementally; both go through the
51
+ * same `outputSuccess` emit path so JSON / summary / --raw / --fields /
52
+ * --jq behavior is identical.
53
+ *
54
+ * @param data The array payload.
55
+ * @param extraMeta Optional resource-specific meta keys (e.g. `query`,
56
+ * `team`). The type excludes `count` — `count` is
57
+ * always computed from `data.length` to preserve the
58
+ * wire-contract invariant.
59
+ */
60
+ export declare function outputList<T>(data: T[], extraMeta?: ListExtraMeta): void;
61
+ /**
62
+ * Typed wrapper around `outputSuccess` for a single-resource response.
63
+ * Pure passthrough — the envelope contract for single resources is just
64
+ * the resource object itself (no `data`/`meta` wrapping). Same emit
65
+ * path as `outputSuccess` and `outputList`; this overload exists so
66
+ * call sites can document their intent at the type level.
67
+ *
68
+ * The conditional return type rejects array inputs at the type level —
69
+ * a caller that meant `outputList` and passed an array gets a compile
70
+ * error pointing them at the right helper. Runtime behavior on an array
71
+ * is unchanged (it'd still emit JSON), but the type catches the foot-gun.
72
+ */
73
+ export declare function outputSingle<T>(data: T extends readonly unknown[] ? "outputSingle does not accept arrays — use outputList(data) instead" : T): void;
7
74
  export declare function outputSuccess(data: unknown): void;
75
+ /**
76
+ * Emit a `results_truncated` warning when a list command's result count
77
+ * equals the requested `--limit`, signaling to the caller (typically an
78
+ * AI agent) that more results may exist beyond the page. Heuristic, not
79
+ * exact — `length === limit` has a false-positive when the workspace
80
+ * happens to hold exactly `limit` matching items. Accurate `hasNextPage`
81
+ * would require widening every list-service return type; deferred until
82
+ * a caller cares about the false-positive rate.
83
+ *
84
+ * Message includes a concrete next-step (suggested `--limit` is 2× the
85
+ * current limit) so the agent doesn't have to derive it.
86
+ */
87
+ export declare function warnIfTruncated(count: number, limit: number): void;
8
88
  export declare function outputWarning(message: string | string[]): void;
9
89
  export declare function resetWarnings(): void;
10
- export declare function resetOutputFormat(): void;
11
90
  export declare function handleAsyncCommand<TArgs extends unknown[]>(asyncFn: (...args: TArgs) => Promise<void>): (...args: TArgs) => Promise<void>;
91
+ export {};
@@ -1,6 +1,7 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { dispatch as dispatchSummary, inferKindFromPayload, } from "./formatters/summary.js";
3
3
  import { logger } from "./logger.js";
4
+ import { sanitizeForLog } from "./sanitize-for-log.js";
4
5
  const warningBuffer = [];
5
6
  let rawMode = false;
6
7
  let jqFilter = null;
@@ -18,6 +19,7 @@ export function setFieldsFilter(fields) {
18
19
  export function setOutputFormat(format) {
19
20
  outputFormat = format;
20
21
  }
22
+ /** @internal Test seam — consumers should not depend on the format state. */
21
23
  export function getOutputFormat() {
22
24
  return outputFormat;
23
25
  }
@@ -38,14 +40,51 @@ function filterFields(obj, fields) {
38
40
  return obj;
39
41
  }
40
42
  /**
41
- * Emit a summary-format render to stdout for the given payload. The
42
- * caller (`outputSuccess`) has already applied `--raw` and `--fields`,
43
- * so the value here is post-filter — no need to unwrap again.
43
+ * Emit a summary-format render to stdout for the given payload.
44
+ *
45
+ * `kind` is captured upstream from the **pre-filter** payload — if we
46
+ * inferred here, `--fields identifier,url` would strip `title` and
47
+ * the heuristic would fall through to "generic", silently breaking
48
+ * the issue-list table. Caching the pre-filter shape keeps the
49
+ * formatter accurate regardless of how the user pared the JSON.
44
50
  */
45
- function emitSummary(payload) {
46
- const kind = inferKindFromPayload(payload);
51
+ function emitSummary(payload, kind) {
47
52
  logger.info(dispatchSummary(kind, payload));
48
53
  }
54
+ /**
55
+ * Typed wrapper around `outputSuccess` for list responses — builds the
56
+ * `{ data, meta: { count, ...extraMeta } }` envelope and emits it. The
57
+ * 81 existing inline `outputSuccess({ data, meta: { count, ... } })`
58
+ * call sites can migrate to this incrementally; both go through the
59
+ * same `outputSuccess` emit path so JSON / summary / --raw / --fields /
60
+ * --jq behavior is identical.
61
+ *
62
+ * @param data The array payload.
63
+ * @param extraMeta Optional resource-specific meta keys (e.g. `query`,
64
+ * `team`). The type excludes `count` — `count` is
65
+ * always computed from `data.length` to preserve the
66
+ * wire-contract invariant.
67
+ */
68
+ export function outputList(data, extraMeta) {
69
+ const meta = { ...extraMeta, count: data.length };
70
+ const envelope = { data, meta };
71
+ outputSuccess(envelope);
72
+ }
73
+ /**
74
+ * Typed wrapper around `outputSuccess` for a single-resource response.
75
+ * Pure passthrough — the envelope contract for single resources is just
76
+ * the resource object itself (no `data`/`meta` wrapping). Same emit
77
+ * path as `outputSuccess` and `outputList`; this overload exists so
78
+ * call sites can document their intent at the type level.
79
+ *
80
+ * The conditional return type rejects array inputs at the type level —
81
+ * a caller that meant `outputList` and passed an array gets a compile
82
+ * error pointing them at the right helper. Runtime behavior on an array
83
+ * is unchanged (it'd still emit JSON), but the type catches the foot-gun.
84
+ */
85
+ export function outputSingle(data) {
86
+ outputSuccess(data);
87
+ }
49
88
  export function outputSuccess(data) {
50
89
  const warnings = drainWarnings();
51
90
  let output;
@@ -58,6 +97,11 @@ export function outputSuccess(data) {
58
97
  else {
59
98
  output = data;
60
99
  }
100
+ // Capture the kind from the original envelope shape BEFORE --raw /
101
+ // --fields stripping. Otherwise filtering away signature fields (e.g.
102
+ // `title` on an issue) breaks shape inference and the summary
103
+ // formatter falls back to the generic key-value dump.
104
+ const inferredKind = outputFormat === "summary" ? inferKindFromPayload(output) : "generic";
61
105
  // --raw: unwrap { data: [...] } to just the array
62
106
  if (rawMode &&
63
107
  output !== null &&
@@ -87,7 +131,7 @@ export function outputSuccess(data) {
87
131
  // as a human-readable block. We bypass the jq path because jq is a
88
132
  // JSON-shape filter — it doesn't compose with text output.
89
133
  if (outputFormat === "summary") {
90
- emitSummary(output);
134
+ emitSummary(output, inferredKind);
91
135
  return;
92
136
  }
93
137
  if (jqFilter) {
@@ -111,6 +155,25 @@ export function outputSuccess(data) {
111
155
  logger.info(JSON.stringify(output, null, 2));
112
156
  }
113
157
  }
158
+ /**
159
+ * Emit a `results_truncated` warning when a list command's result count
160
+ * equals the requested `--limit`, signaling to the caller (typically an
161
+ * AI agent) that more results may exist beyond the page. Heuristic, not
162
+ * exact — `length === limit` has a false-positive when the workspace
163
+ * happens to hold exactly `limit` matching items. Accurate `hasNextPage`
164
+ * would require widening every list-service return type; deferred until
165
+ * a caller cares about the false-positive rate.
166
+ *
167
+ * Message includes a concrete next-step (suggested `--limit` is 2× the
168
+ * current limit) so the agent doesn't have to derive it.
169
+ */
170
+ export function warnIfTruncated(count, limit) {
171
+ if (count !== limit) {
172
+ return;
173
+ }
174
+ outputWarning(`results_truncated: returned ${count} matching --limit ${limit}; more results may exist. ` +
175
+ `Re-run with --limit ${limit * 2} (or narrow via filters like --name, --state, --team) to verify.`);
176
+ }
114
177
  export function outputWarning(message) {
115
178
  const messages = Array.isArray(message) ? message : [message];
116
179
  for (const msg of messages) {
@@ -127,16 +190,18 @@ function drainWarnings() {
127
190
  export function resetWarnings() {
128
191
  warningBuffer.length = 0;
129
192
  }
130
- export function resetOutputFormat() {
131
- outputFormat = "json";
132
- }
133
193
  function outputError(error) {
134
- const payload = JSON.stringify({ error: error.message }, null, 2);
194
+ // Run the message and stack through sanitizeForLog so a future SDK
195
+ // upgrade (or proxy/MITM error body) that embeds `lin_api_…` /
196
+ // `lin_oauth_…` / `Bearer <payload>` in error text can't leak a token
197
+ // into stdout, shell history, or CI logs. The wizard already sanitizes
198
+ // its own log paths; this is the central error path on every command.
199
+ const payload = JSON.stringify({ error: sanitizeForLog(error.message) }, null, 2);
135
200
  // Write to stdout (same channel as success) so machine callers always
136
201
  // receive exactly one parseable JSON object regardless of stream capture.
137
202
  logger.info(payload);
138
203
  if (process.env.EL_LINEAR_DEBUG ?? process.env.LINCTL_DEBUG) {
139
- logger.error(error.stack ?? "");
204
+ logger.error(sanitizeForLog(error.stack ?? ""));
140
205
  }
141
206
  process.exit(1);
142
207
  }