dsh-atlassian-plugin 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -193,10 +193,13 @@ var OAuthServer = class {
193
193
  */
194
194
  toTokenFields = (payload, previous) => {
195
195
  const expiresIn = Number(payload.expires_in);
196
+ const accessToken = payload.access_token;
197
+ const refreshToken = payload.refresh_token;
198
+ const tokenType = payload.token_type;
196
199
  return {
197
- accessToken: typeof payload.access_token === "string" ? payload.access_token : previous.accessToken,
198
- refreshToken: typeof payload.refresh_token === "string" ? payload.refresh_token : previous.refreshToken,
199
- tokenType: typeof payload.token_type === "string" ? payload.token_type : previous.tokenType,
200
+ accessToken: typeof accessToken === "string" && accessToken !== "" ? accessToken : previous.accessToken,
201
+ refreshToken: typeof refreshToken === "string" && refreshToken !== "" ? refreshToken : previous.refreshToken,
202
+ tokenType: typeof tokenType === "string" && tokenType !== "" ? tokenType : previous.tokenType,
200
203
  scope: typeof payload.scope === "string" ? payload.scope : previous.scope,
201
204
  expiresAt: Date.now() + (Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : 3600) * 1e3
202
205
  };
@@ -234,16 +237,28 @@ async function discover(profile) {
234
237
  }
235
238
  if (metadata === void 0)
236
239
  throw new Error(`no OAuth metadata for ${endpoint} (issuer ${issuer})`);
237
- const authorizationEndpoint = firstString(metadata.authorization_endpoint);
238
- const tokenEndpoint = firstString(metadata.token_endpoint);
240
+ const authorizationEndpoint = secureEndpoint(
241
+ firstString(metadata.authorization_endpoint),
242
+ "the authorization endpoint"
243
+ );
244
+ const tokenEndpoint = secureEndpoint(
245
+ firstString(metadata.token_endpoint),
246
+ "the token endpoint"
247
+ );
239
248
  if (authorizationEndpoint === void 0 || tokenEndpoint === void 0)
240
249
  throw new Error(`incomplete OAuth metadata for ${issuer}`);
241
250
  const advertised = stringList(protectedResource?.scopes_supported) ?? stringList(metadata.scopes_supported);
242
251
  const scopes = chooseScopes(profile, advertised);
243
- const registrationEndpoint = firstString(metadata.registration_endpoint);
244
- const revocationEndpoint = firstString(metadata.revocation_endpoint);
252
+ const registrationEndpoint = secureEndpoint(
253
+ firstString(metadata.registration_endpoint),
254
+ "the registration endpoint"
255
+ );
256
+ const revocationEndpoint = secureEndpoint(
257
+ firstString(metadata.revocation_endpoint),
258
+ "the revocation endpoint"
259
+ );
245
260
  return new OAuthServer({
246
- issuer: firstString(metadata.issuer) ?? issuer,
261
+ issuer: secureEndpoint(firstString(metadata.issuer) ?? issuer, "the issuer") ?? issuer,
247
262
  resource: endpoint,
248
263
  authorizationEndpoint,
249
264
  tokenEndpoint,
@@ -283,6 +298,19 @@ function chooseScopes(profile, advertised) {
283
298
  if (chosen.length > 0) return chosen;
284
299
  return advertised.includes("offline_access") ? ["offline_access"] : [...profile.oauth.fallbackScopes];
285
300
  }
301
+ function secureEndpoint(value, what) {
302
+ if (value === void 0) return void 0;
303
+ let url;
304
+ try {
305
+ url = new URL(value);
306
+ } catch {
307
+ throw new Error(`${what} is not a URL`);
308
+ }
309
+ const loopback = url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "[::1]";
310
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback))
311
+ throw new Error(`${what} must use https`);
312
+ return value;
313
+ }
286
314
  function stringList(value) {
287
315
  if (!Array.isArray(value)) return void 0;
288
316
  const items = value.filter(
@@ -343,6 +371,8 @@ var McpBridge = class {
343
371
  names = [];
344
372
  ready = false;
345
373
  failure;
374
+ pending;
375
+ logger;
346
376
  /** Public names of the published tools, in discovery order. */
347
377
  get toolNames() {
348
378
  return this.names;
@@ -367,7 +397,15 @@ var McpBridge = class {
367
397
  * @returns whether the bridge is usable; failures are logged, never thrown.
368
398
  */
369
399
  async ensure(ctx) {
400
+ this.pending ??= this.connectOnce(ctx).finally(() => {
401
+ this.pending = void 0;
402
+ });
403
+ return await this.pending;
404
+ }
405
+ /** One connection attempt, shared by every caller that arrives meanwhile. */
406
+ async connectOnce(ctx) {
370
407
  const { profile, store } = this.options;
408
+ this.logger = loggerOf(ctx);
371
409
  if (needsOAuth(profile)) {
372
410
  const grant = await store?.read();
373
411
  if (grant === void 0 || !grant.isSignedIn()) return false;
@@ -420,10 +458,18 @@ var McpBridge = class {
420
458
  throw new Error(
421
459
  `the ${this.options.profile.label} MCP connection is not open`
422
460
  );
423
- return await client.callTool(
461
+ const result = await client.callTool(
424
462
  { name: name2, arguments: args },
425
463
  { timeout: TOOL_CALL_TIMEOUT_MS }
426
464
  );
465
+ if (this.options.profile.debug)
466
+ this.logger?.info(
467
+ "[%s] %s answered %s",
468
+ accountLabel(this.options.profile),
469
+ name2,
470
+ JSON.stringify(result)
471
+ );
472
+ return result;
427
473
  }
428
474
  /** The discovered authorization server, remembered across reconnects. */
429
475
  async authorizationServer(profile) {
@@ -459,38 +505,49 @@ var McpBridge = class {
459
505
  const listed = await client.listTools(void 0, { cacheMode: "refresh" });
460
506
  const disposers = /* @__PURE__ */ new Map();
461
507
  const names = [];
462
- for (const tool of listed.tools) {
463
- const name2 = publicToolName(
464
- tool.name,
465
- this.options.profile.toolNamespace
466
- );
467
- const base = createMcpToolDefinition(ctx, {
468
- name: name2,
469
- rawName: tool.name,
470
- description: tool.description ?? `MCP tool ${tool.name}`,
471
- inputSchema: tool.inputSchema ?? {
472
- type: "object",
473
- properties: {}
474
- },
475
- ...tool.outputSchema === void 0 ? {} : { outputSchema: tool.outputSchema },
476
- ...tool.execution?.taskSupport === "required" ? { taskRequired: true } : {},
477
- call: async (args, execution) => await client.callTool(
478
- { name: tool.name, arguments: args },
479
- {
480
- signal: execution.signal,
481
- toolDefinition: tool,
482
- timeout: TOOL_CALL_TIMEOUT_MS
483
- }
484
- )
485
- });
486
- disposers.set(
487
- name2,
488
- ctx.tools.register(this.options.decorate?.(base, tool) ?? base)
489
- );
490
- names.push(name2);
491
- }
492
- for (const dispose of this.disposers.values()) dispose();
508
+ const previous = this.disposers;
493
509
  this.disposers = disposers;
510
+ this.names = [];
511
+ for (const dispose of previous.values()) dispose();
512
+ try {
513
+ for (const tool of listed.tools) {
514
+ const name2 = publicToolName(
515
+ tool.name,
516
+ this.options.profile.toolNamespace
517
+ );
518
+ const base = createMcpToolDefinition(ctx, {
519
+ name: name2,
520
+ rawName: tool.name,
521
+ description: tool.description ?? `MCP tool ${tool.name}`,
522
+ inputSchema: tool.inputSchema ?? {
523
+ type: "object",
524
+ properties: {}
525
+ },
526
+ ...tool.outputSchema === void 0 ? {} : { outputSchema: tool.outputSchema },
527
+ ...tool.execution?.taskSupport === "required" ? { taskRequired: true } : {},
528
+ call: async (args, execution) => await client.callTool(
529
+ { name: tool.name, arguments: args },
530
+ {
531
+ signal: execution.signal,
532
+ toolDefinition: tool,
533
+ timeout: TOOL_CALL_TIMEOUT_MS
534
+ }
535
+ )
536
+ });
537
+ disposers.set(
538
+ name2,
539
+ ctx.tools.register(
540
+ this.options.decorate?.(base, tool) ?? base
541
+ )
542
+ );
543
+ names.push(name2);
544
+ }
545
+ } catch (error) {
546
+ for (const dispose of disposers.values()) dispose();
547
+ this.disposers = /* @__PURE__ */ new Map();
548
+ this.names = [];
549
+ throw error;
550
+ }
494
551
  this.names = names;
495
552
  return names;
496
553
  }
@@ -499,8 +556,8 @@ var McpBridge = class {
499
556
  // ../mcp-oauth/src/browser.ts
500
557
  import { execFile } from "node:child_process";
501
558
  function openBrowser(ctx, url) {
502
- const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
503
- const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
559
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
560
+ const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
504
561
  execFile(command, args, (error) => {
505
562
  if (error !== null)
506
563
  loggerOf(ctx).warn(
@@ -681,6 +738,17 @@ var GrantStore = class {
681
738
  // ../mcp-oauth/src/loopback.ts
682
739
  import { timingSafeEqual } from "node:crypto";
683
740
  import { createServer } from "node:http";
741
+ var HTML_ENTITIES = {
742
+ "&": "&",
743
+ "<": "&lt;",
744
+ ">": "&gt;",
745
+ '"': "&quot;",
746
+ "'": "&#39;"
747
+ };
748
+ var escapeHtml = (value) => value.replace(
749
+ /[&<>"']/gu,
750
+ (character) => HTML_ENTITIES[character] ?? character
751
+ );
684
752
  var LoopbackListener = class _LoopbackListener {
685
753
  server;
686
754
  port;
@@ -773,7 +841,9 @@ var LoopbackListener = class _LoopbackListener {
773
841
  });
774
842
  };
775
843
  static callbackPage = (title, message) => {
776
- return `<!doctype html><meta charset="utf-8"><title>${title}</title><body style="font:16px/1.5 system-ui;margin:4rem auto;max-width:32rem;text-align:center"><h1 style="font-size:1.25rem">${title}</h1><p>${message}</p></body>`;
844
+ const safeTitle = escapeHtml(title);
845
+ const safeMessage = escapeHtml(message);
846
+ return `<!doctype html><meta charset="utf-8"><title>${safeTitle}</title><body style="font:16px/1.5 system-ui;margin:4rem auto;max-width:32rem;text-align:center"><h1 style="font-size:1.25rem">${safeTitle}</h1><p>${safeMessage}</p></body>`;
777
847
  };
778
848
  };
779
849
 
@@ -844,6 +914,8 @@ var AuthorizationFlow = class {
844
914
  redirect_uri: listener.redirectUri,
845
915
  code_verifier: verifier
846
916
  });
917
+ if (typeof payload.access_token !== "string" || payload.access_token === "")
918
+ throw new Error("the token endpoint returned no access token");
847
919
  const base = {
848
920
  ...existing ?? { version: 1 },
849
921
  version: 1,
@@ -875,7 +947,7 @@ var AuthorizationFlow = class {
875
947
  * reused only when the resource and the URI both still match.
876
948
  */
877
949
  async clientFor(oauth, redirectUri, existing) {
878
- if (existing !== void 0 && existing.resource === this.profile.endpoint && existing.redirectUri === redirectUri && typeof existing.clientId === "string") {
950
+ if (existing !== void 0 && existing.issuer === oauth.issuer && existing.resource === this.profile.endpoint && existing.redirectUri === redirectUri && typeof existing.clientId === "string") {
879
951
  const tokenAuthMethod = authMethodOf(existing);
880
952
  return existing.clientSecret === void 0 ? { clientId: existing.clientId, tokenAuthMethod } : {
881
953
  clientId: existing.clientId,
@@ -1107,10 +1179,8 @@ var createIntegrationCommand = (spec, auth) => {
1107
1179
  if (action === "login") {
1108
1180
  void auth.signIn();
1109
1181
  await auth.settleNotice();
1110
- return {
1111
- kind: "success",
1112
- text: auth.loginMessage(await auth.getStatus())
1113
- };
1182
+ const status2 = await auth.getStatus();
1183
+ return status2.error === null ? { kind: "success", text: auth.loginMessage(status2) } : { kind: "error", text: status2.error };
1114
1184
  }
1115
1185
  if (action === "logout") {
1116
1186
  await auth.signOut();
@@ -1128,11 +1198,17 @@ var createIntegrationCommand = (spec, auth) => {
1128
1198
  };
1129
1199
  }
1130
1200
  const status = await auth.getStatus();
1201
+ if (status.error !== null) return { kind: "error", text: status.error };
1131
1202
  if (!status.signedIn)
1132
1203
  return {
1133
1204
  kind: "success",
1134
1205
  text: `Not signed in to ${spec.label}. Run /${spec.name} login.`
1135
1206
  };
1207
+ if (!status.connected)
1208
+ return {
1209
+ kind: "success",
1210
+ text: `${spec.label} is signed in but not connected. Run /${spec.name} sync.`
1211
+ };
1136
1212
  const until = status.expiresAt === null ? "unknown" : new Date(status.expiresAt).toLocaleString();
1137
1213
  const extra = spec.describe?.(status) ?? "";
1138
1214
  const tail = extra === "" ? "" : ` ${extra}`;
@@ -1243,6 +1319,10 @@ var stripHtmlComments = (markdown) => {
1243
1319
  return kept.join("\n");
1244
1320
  };
1245
1321
 
1322
+ // ../integration-host/src/status.ts
1323
+ var CLIENT_REQUEST_HEADER = "x-dsh-client";
1324
+ var CLIENT_REQUEST_VALUE = "1";
1325
+
1246
1326
  // ../integration-host/src/route.ts
1247
1327
  var MAX_BODY_BYTES = 8 * 1024;
1248
1328
  function mountIntegrationRoute(ctx, spec) {
@@ -1280,7 +1360,7 @@ async function readJson(request) {
1280
1360
  const buffer = chunk;
1281
1361
  size += buffer.length;
1282
1362
  if (size > MAX_BODY_BYTES)
1283
- throw new Error("request body is larger than this route accepts");
1363
+ throw new BadRequest("request body is larger than this route accepts");
1284
1364
  chunks.push(buffer);
1285
1365
  }
1286
1366
  if (chunks.length === 0) return void 0;
@@ -1291,6 +1371,34 @@ async function readJson(request) {
1291
1371
  return void 0;
1292
1372
  }
1293
1373
  }
1374
+ var READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
1375
+ function webOrigin(value) {
1376
+ try {
1377
+ const url = new URL(value);
1378
+ return url.protocol === "http:" || url.protocol === "https:" ? url : void 0;
1379
+ } catch {
1380
+ return void 0;
1381
+ }
1382
+ }
1383
+ function clientRequestRefusal(request) {
1384
+ const method = request.method ?? "GET";
1385
+ if (READ_METHODS.has(method)) return void 0;
1386
+ const headers = request.headers ?? {};
1387
+ if (headers[CLIENT_REQUEST_HEADER] !== CLIENT_REQUEST_VALUE)
1388
+ return "the request did not come from the harness page";
1389
+ const contentType = headers["content-type"];
1390
+ if (typeof contentType !== "string" || !contentType.toLowerCase().startsWith("application/json"))
1391
+ return "an integration write must carry a JSON body";
1392
+ const origin = headers.origin;
1393
+ const host = headers.host;
1394
+ const originUrl = typeof origin === "string" ? webOrigin(origin) : void 0;
1395
+ if (originUrl !== void 0 && typeof host === "string" && originUrl.host !== host)
1396
+ return "the request came from another origin";
1397
+ const site = headers["sec-fetch-site"];
1398
+ if (originUrl !== void 0 && typeof site === "string" && site.toLowerCase() === "cross-site")
1399
+ return "cross-site requests are refused";
1400
+ return void 0;
1401
+ }
1294
1402
  function createIntegrationRoute(spec) {
1295
1403
  const { prefix, auth } = spec;
1296
1404
  return async (request, response) => {
@@ -1318,6 +1426,11 @@ function createIntegrationRoute(spec) {
1318
1426
  send(502, { error: errorText(error) });
1319
1427
  }
1320
1428
  };
1429
+ const refusal = clientRequestRefusal(request);
1430
+ if (refusal !== void 0) {
1431
+ send(403, { error: refusal });
1432
+ return;
1433
+ }
1321
1434
  let body;
1322
1435
  const context = {
1323
1436
  url,
@@ -1748,7 +1861,16 @@ var Extractor = class {
1748
1861
  };
1749
1862
  /** Collapse the whitespace a markdown or ADF body is full of. */
1750
1863
  tidy = (value) => {
1751
- return value.replace(/\r\n?/gu, "\n").replace(/[^\S\n]+/gu, " ").replace(/\n{3,}/gu, "\n\n").trim().slice(0, this.MAX_BODY);
1864
+ const fences = [];
1865
+ const protectedText = value.replace(/(```|~~~)[\s\S]*?\1/gu, (block) => {
1866
+ fences.push(block);
1867
+ return `\uE000${fences.length - 1}\uE000`;
1868
+ });
1869
+ const tidied = protectedText.replace(/\r\n?/gu, "\n").replace(/[^\S\n]+/gu, " ").replace(/\n{3,}/gu, "\n\n").trim().slice(0, this.MAX_BODY);
1870
+ return tidied.replace(
1871
+ /\uE000(\d+)\uE000/gu,
1872
+ (_placeholder, index) => fences[Number(index)] ?? ""
1873
+ );
1752
1874
  };
1753
1875
  asRecord = (value) => {
1754
1876
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
@@ -1870,7 +1992,9 @@ var ISSUE_TOOL = "getJiraIssue";
1870
1992
  var ISSUE_FIELDS = ["summary", "status", "assignee", "issuetype", "priority"];
1871
1993
  var nameOf = (value) => typeof value === "string" && value !== "" ? value : pickString(asRecord(value) ?? {}, "displayName", "name", "value");
1872
1994
  var factOf = (key, value, siteUrl) => {
1873
- const record = asRecord(value) ?? {};
1995
+ const record = mcpJson(value).map((root) => asRecord(root)).find(
1996
+ (candidate) => candidate !== void 0 && (candidate.fields !== void 0 || candidate.key !== void 0)
1997
+ ) ?? {};
1874
1998
  const fields = asRecord(record.fields) ?? record;
1875
1999
  const identity = pickString(record, "key") ?? key;
1876
2000
  const summary = extractor.text(pickString(fields, "summary"));
@@ -2088,7 +2212,7 @@ var PanelApi = class {
2088
2212
  space: pickString(asRecord(record.space) ?? record.space, "name", "key"),
2089
2213
  url: webui === void 0 ? this.deps.state.facts?.siteUrl === void 0 ? void 0 : `${this.deps.state.facts?.siteUrl}/wiki/pages/viewpage.action?pageId=${id}` : this.wikiUrl(webui, this.deps.state.facts?.siteUrl),
2090
2214
  body: extractor.text(record.body),
2091
- comments: this.commentsOf(asRecord(comments)?.results)
2215
+ comments: this.commentsOf(this.firstRecord(comments)?.results)
2092
2216
  };
2093
2217
  };
2094
2218
  /** People the site matches to one search string. */
@@ -2101,7 +2225,7 @@ var PanelApi = class {
2101
2225
  const users = this.userList(value);
2102
2226
  return users.map((user) => ({
2103
2227
  name: pickString(user, "displayName") ?? "",
2104
- accountId: pickString(user, "accountId") ?? ""
2228
+ accountId: pickString(user, "accountId", "account_id") ?? ""
2105
2229
  })).filter((person) => person.name !== "" && person.accountId !== "").slice(0, PEOPLE_LIMIT);
2106
2230
  };
2107
2231
  /**
@@ -2162,7 +2286,7 @@ var PanelApi = class {
2162
2286
  };
2163
2287
  personOf = (value) => {
2164
2288
  const record = asRecord(value);
2165
- const accountId = pickString(record, "accountId");
2289
+ const accountId = pickString(record, "accountId", "account_id");
2166
2290
  const name2 = pickString(record, "displayName", "name");
2167
2291
  if (accountId === void 0 || name2 === void 0) return void 0;
2168
2292
  return { accountId, name: name2 };
@@ -2415,6 +2539,12 @@ ${body}`);
2415
2539
  }
2416
2540
 
2417
2541
  // src/host/tools/present.ts
2542
+ function hasMore(value) {
2543
+ return mcpJson(value).some((root) => {
2544
+ const record = asRecord(root);
2545
+ return record?.isLast === false || typeof record?.nextPageToken === "string" && record.nextPageToken !== "";
2546
+ });
2547
+ }
2418
2548
  function stringList2(value) {
2419
2549
  if (!Array.isArray(value)) return [];
2420
2550
  return value.filter(
@@ -2521,6 +2651,7 @@ Could not resolve: ${outcome.failed.join(", ")}`;
2521
2651
  ...outcome.title === void 0 ? {} : { title: outcome.title },
2522
2652
  layout: outcome.layout,
2523
2653
  ...outcome.total === void 0 ? {} : { total: outcome.total },
2654
+ ...outcome.truncated ? { truncated: true } : {},
2524
2655
  items: outcome.items
2525
2656
  };
2526
2657
  return {
@@ -2552,9 +2683,13 @@ Could not resolve: ${outcome.failed.join(", ")}`;
2552
2683
  throw new Error(
2553
2684
  "no Atlassian site known yet; run a Jira or Confluence search first, or sign in again"
2554
2685
  );
2686
+ const requested = args.issues.length > 0 || args.pages.length > 0 || args.jql !== void 0 || args.cql !== void 0;
2687
+ if (!requested)
2688
+ throw new Error("nothing to show: pass issues, pages, jql, or cql");
2555
2689
  const items = [];
2556
2690
  const failed = [];
2557
2691
  let matched = 0;
2692
+ let truncated = false;
2558
2693
  const push = (candidate) => {
2559
2694
  for (const item of candidate) {
2560
2695
  if (items.length >= this.MAX_ITEMS) return;
@@ -2585,6 +2720,7 @@ Could not resolve: ${outcome.failed.join(", ")}`;
2585
2720
  const found = this.issuesOf(value, deps.state.facts?.siteUrl);
2586
2721
  push(found.items);
2587
2722
  matched += found.total ?? found.items.length;
2723
+ if (hasMore(value)) truncated = true;
2588
2724
  }
2589
2725
  for (const id of args.pages) {
2590
2726
  try {
@@ -2608,9 +2744,8 @@ Could not resolve: ${outcome.failed.join(", ")}`;
2608
2744
  const found = this.pagesOf(value, deps.state.facts?.siteUrl);
2609
2745
  push(found.items);
2610
2746
  matched += found.total ?? found.items.length;
2747
+ if (hasMore(value)) truncated = true;
2611
2748
  }
2612
- if (items.length === 0 && failed.length === 0)
2613
- throw new Error("nothing to show: pass issues, pages, jql, or cql");
2614
2749
  const gists = await this.summarize(deps.ctx, items, exec);
2615
2750
  const projected = items.map(
2616
2751
  (item, index) => item.kind === "issue" ? {
@@ -2626,7 +2761,8 @@ Could not resolve: ${outcome.failed.join(", ")}`;
2626
2761
  layout: this.resolveLayout(args.layout, projected.length),
2627
2762
  items: projected,
2628
2763
  total: matched > projected.length ? matched : void 0,
2629
- failed
2764
+ failed,
2765
+ truncated
2630
2766
  };
2631
2767
  };
2632
2768
  /**
@@ -2725,6 +2861,12 @@ Could not resolve: ${outcome.failed.join(", ")}`;
2725
2861
  }
2726
2862
  /** The text the model reads: what the card shows, in order. */
2727
2863
  describe(outcome, args) {
2864
+ if (outcome.items.length === 0) {
2865
+ const lines2 = ["No issues or pages matched."];
2866
+ if (args.jql !== void 0) lines2.push(`JQL: ${args.jql}`);
2867
+ if (args.cql !== void 0) lines2.push(`CQL: ${args.cql}`);
2868
+ return lines2.join("\n");
2869
+ }
2728
2870
  const issues = outcome.items.filter((item) => item.kind === "issue");
2729
2871
  const pages = outcome.items.filter((item) => item.kind === "page");
2730
2872
  const counts = [
@@ -2755,6 +2897,8 @@ Could not resolve: ${outcome.failed.join(", ")}`;
2755
2897
  }
2756
2898
  if (args.jql !== void 0) lines.push(`JQL: ${args.jql}`);
2757
2899
  if (args.cql !== void 0) lines.push(`CQL: ${args.cql}`);
2900
+ if (outcome.truncated)
2901
+ lines.push("The search had more results than this card read.");
2758
2902
  return lines.join("\n");
2759
2903
  }
2760
2904
  };