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/client.js +48 -30
- package/lib/client.js.map +4 -4
- package/lib/index.js +201 -57
- package/lib/index.js.map +4 -4
- package/package.json +5 -5
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
|
|
198
|
-
refreshToken: typeof
|
|
199
|
-
tokenType: typeof
|
|
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 =
|
|
238
|
-
|
|
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 =
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" ? "
|
|
503
|
-
const args = process.platform === "win32" ? ["
|
|
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
|
+
"<": "<",
|
|
744
|
+
">": ">",
|
|
745
|
+
'"': """,
|
|
746
|
+
"'": "'"
|
|
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
|
-
|
|
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
|
-
|
|
1111
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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
|
};
|