dsh-atlassian-plugin 0.0.0-stage → 0.3.1

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 ADDED
@@ -0,0 +1,2836 @@
1
+ // ../mcp-oauth/src/account.ts
2
+ var DEFAULT_INSTANCE = "default";
3
+ var INSTANCE_PATTERN = /^[a-z][a-z0-9-]*$/;
4
+ var idFor = (instance) => instance === DEFAULT_INSTANCE ? "mcp" : `mcp-${instance}`;
5
+ function credentialKeyFor(scope, instance = DEFAULT_INSTANCE) {
6
+ for (const segment of [scope, instance])
7
+ if (!INSTANCE_PATTERN.test(segment))
8
+ throw new TypeError(
9
+ `credential key segment "${segment}" must match ${String(INSTANCE_PATTERN)}`
10
+ );
11
+ return `${scope}/${idFor(instance)}`;
12
+ }
13
+
14
+ // ../mcp-oauth/src/config.ts
15
+ var REQUEST_TIMEOUT_MS = 3e4;
16
+ var PORT_ATTEMPTS = 20;
17
+ var SIGN_IN_TIMEOUT_MS = 5 * 6e4;
18
+ var REFRESH_MARGIN_MS = 6e4;
19
+
20
+ // ../mcp-oauth/src/log.ts
21
+ function loggerOf(ctx) {
22
+ const logger = ctx.logger;
23
+ if (typeof logger?.info === "function" && typeof logger.warn === "function") {
24
+ return {
25
+ info: (message, ...args) => logger.info?.(message, ...args),
26
+ warn: (message, ...args) => logger.warn?.(message, ...args)
27
+ };
28
+ }
29
+ return {
30
+ info: (message, ...args) => console.info(message, ...args),
31
+ warn: (message, ...args) => console.warn(message, ...args)
32
+ };
33
+ }
34
+ function errorText(error) {
35
+ return error instanceof Error ? error.message : String(error);
36
+ }
37
+
38
+ // ../mcp-oauth/src/net.ts
39
+ var Network = class {
40
+ /** A JSON GET, or undefined when the document is absent or unreadable. */
41
+ getJSON = async (url) => {
42
+ try {
43
+ const response = await fetch(url, {
44
+ headers: { accept: "application/json" },
45
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
46
+ });
47
+ if (!response.ok) return void 0;
48
+ const body = await response.json();
49
+ return body !== null && typeof body === "object" ? body : void 0;
50
+ } catch {
51
+ return void 0;
52
+ }
53
+ };
54
+ /** A JSON POST that must succeed; the response body is returned as JSON. */
55
+ postJSON = async (url, body, what) => {
56
+ let response;
57
+ try {
58
+ response = await fetch(url, {
59
+ method: "POST",
60
+ headers: {
61
+ "content-type": "application/json",
62
+ accept: "application/json"
63
+ },
64
+ body: JSON.stringify(body),
65
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
66
+ });
67
+ } catch (error) {
68
+ throw new Error(`${what} failed: ${errorText(error)}`);
69
+ }
70
+ const text = await response.text();
71
+ if (!response.ok)
72
+ throw new Error(`${what} failed (${response.status}): ${text}`);
73
+ const parsed = JSON.parse(text);
74
+ if (parsed === null || typeof parsed !== "object")
75
+ throw new Error(`${what} returned a non-object`);
76
+ return parsed;
77
+ };
78
+ /** A form POST to a token endpoint, with the same deadline discipline. */
79
+ postForm = async (url, fields, what, authorization) => {
80
+ let response;
81
+ try {
82
+ response = await fetch(url, {
83
+ method: "POST",
84
+ headers: {
85
+ "content-type": "application/x-www-form-urlencoded",
86
+ accept: "application/json",
87
+ ...authorization === void 0 ? {} : { authorization }
88
+ },
89
+ body: new URLSearchParams(fields),
90
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
91
+ });
92
+ } catch (error) {
93
+ throw new Error(`${what} failed: ${errorText(error)}`);
94
+ }
95
+ const text = await response.text();
96
+ if (!response.ok)
97
+ throw new Error(`${what} failed (${response.status}): ${text}`);
98
+ const parsed = JSON.parse(text);
99
+ if (parsed === null || typeof parsed !== "object")
100
+ throw new Error(`${what} returned a non-object`);
101
+ return parsed;
102
+ };
103
+ };
104
+ var network = new Network();
105
+
106
+ // ../mcp-oauth/src/server.ts
107
+ function chooseTokenAuthMethod(advertised) {
108
+ if (advertised === void 0 || advertised.includes("none")) return "none";
109
+ if (advertised.includes("client_secret_post")) return "client_secret_post";
110
+ if (advertised.includes("client_secret_basic")) return "client_secret_basic";
111
+ return "none";
112
+ }
113
+ function authMethodOf(client) {
114
+ if (client.tokenAuthMethod !== void 0) return client.tokenAuthMethod;
115
+ return client.clientSecret === void 0 ? "none" : "client_secret_post";
116
+ }
117
+ var OAuthServer = class {
118
+ /** Issuer identifier, used to detect a generation switch. */
119
+ issuer;
120
+ /** MCP resource URL the tokens are scoped to (RFC 8707 `resource`). */
121
+ resource;
122
+ authorizationEndpoint;
123
+ tokenEndpoint;
124
+ registrationEndpoint;
125
+ revocationEndpoint;
126
+ scopes;
127
+ /** The space-delimited `scope` parameter. */
128
+ scopeString;
129
+ /** Client authentication to ask for at registration. */
130
+ tokenAuthMethod;
131
+ constructor(params) {
132
+ this.issuer = params.issuer;
133
+ this.resource = params.resource;
134
+ this.authorizationEndpoint = params.authorizationEndpoint;
135
+ this.tokenEndpoint = params.tokenEndpoint;
136
+ this.registrationEndpoint = params.registrationEndpoint;
137
+ this.revocationEndpoint = params.revocationEndpoint;
138
+ this.scopes = params.scopes;
139
+ this.scopeString = params.scopeString;
140
+ this.tokenAuthMethod = params.tokenAuthMethod;
141
+ }
142
+ /** Register this installation as a client (RFC 7591). */
143
+ registerClient = async (redirectUri, clientName) => {
144
+ if (this.registrationEndpoint === void 0)
145
+ throw new Error(`${this.issuer} offers no dynamic client registration`);
146
+ const body = await network.postJSON(
147
+ this.registrationEndpoint,
148
+ {
149
+ client_name: clientName,
150
+ redirect_uris: [redirectUri],
151
+ grant_types: ["authorization_code", "refresh_token"],
152
+ response_types: ["code"],
153
+ token_endpoint_auth_method: this.tokenAuthMethod,
154
+ scope: this.scopeString
155
+ },
156
+ "dynamic client registration"
157
+ );
158
+ const clientId = body.client_id;
159
+ if (typeof clientId !== "string" || clientId === "")
160
+ throw new Error("client registration returned no client_id");
161
+ const clientSecret = typeof body.client_secret === "string" ? body.client_secret : void 0;
162
+ const tokenAuthMethod = readTokenAuthMethod(body.token_endpoint_auth_method) ?? (clientSecret === void 0 ? "none" : this.tokenAuthMethod);
163
+ return clientSecret === void 0 ? { clientId, tokenAuthMethod } : { clientId, clientSecret, tokenAuthMethod };
164
+ };
165
+ /** Exchange an authorization code, or a refresh token, for tokens. */
166
+ tokenRequest = async (client, body) => {
167
+ const { clientId, clientSecret } = client;
168
+ const method = authMethodOf(client);
169
+ const fields = {
170
+ ...body,
171
+ resource: this.resource
172
+ };
173
+ if (method === "client_secret_basic") {
174
+ if (clientId === void 0 || clientSecret === void 0)
175
+ throw new Error("client_secret_basic needs a client id and secret");
176
+ return await network.postForm(
177
+ this.tokenEndpoint,
178
+ fields,
179
+ "token request",
180
+ basicAuth(clientId, clientSecret)
181
+ );
182
+ }
183
+ if (clientId !== void 0) fields.client_id = clientId;
184
+ if (method === "client_secret_post" && clientSecret !== void 0)
185
+ fields.client_secret = clientSecret;
186
+ return await network.postForm(this.tokenEndpoint, fields, "token request");
187
+ };
188
+ /**
189
+ * Map a token-endpoint response onto grant fields, keeping what the response
190
+ * omits. Servers that rotate refresh tokens send a new one, and a response
191
+ * that carries one replaces the stored value; one that does not leaves it
192
+ * alone.
193
+ */
194
+ toTokenFields = (payload, previous) => {
195
+ const expiresIn = Number(payload.expires_in);
196
+ 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
+ scope: typeof payload.scope === "string" ? payload.scope : previous.scope,
201
+ expiresAt: Date.now() + (Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : 3600) * 1e3
202
+ };
203
+ };
204
+ };
205
+ function basicAuth(clientId, clientSecret) {
206
+ const credentials = `${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`;
207
+ return `Basic ${Buffer.from(credentials).toString("base64")}`;
208
+ }
209
+ function readTokenAuthMethod(value) {
210
+ return value === "none" || value === "client_secret_post" || value === "client_secret_basic" ? value : void 0;
211
+ }
212
+
213
+ // ../mcp-oauth/src/discovery.ts
214
+ async function discover(profile) {
215
+ const endpoint = profile.endpoint;
216
+ let protectedResource;
217
+ let issuer = new URL(endpoint).origin;
218
+ for (const candidate of protectedResourceCandidates(endpoint)) {
219
+ const document = await network.getJSON(candidate);
220
+ const servers = stringList(document?.authorization_servers);
221
+ if (document !== void 0 && servers !== void 0) {
222
+ protectedResource = document;
223
+ issuer = servers[0] ?? issuer;
224
+ break;
225
+ }
226
+ }
227
+ let metadata;
228
+ for (const candidate of authorizationServerCandidates(issuer)) {
229
+ const document = await network.getJSON(candidate);
230
+ if (firstString(document?.token_endpoint) !== void 0) {
231
+ metadata = document;
232
+ break;
233
+ }
234
+ }
235
+ if (metadata === void 0)
236
+ throw new Error(`no OAuth metadata for ${endpoint} (issuer ${issuer})`);
237
+ const authorizationEndpoint = firstString(metadata.authorization_endpoint);
238
+ const tokenEndpoint = firstString(metadata.token_endpoint);
239
+ if (authorizationEndpoint === void 0 || tokenEndpoint === void 0)
240
+ throw new Error(`incomplete OAuth metadata for ${issuer}`);
241
+ const advertised = stringList(protectedResource?.scopes_supported) ?? stringList(metadata.scopes_supported);
242
+ const scopes = chooseScopes(profile, advertised);
243
+ const registrationEndpoint = firstString(metadata.registration_endpoint);
244
+ const revocationEndpoint = firstString(metadata.revocation_endpoint);
245
+ return new OAuthServer({
246
+ issuer: firstString(metadata.issuer) ?? issuer,
247
+ resource: endpoint,
248
+ authorizationEndpoint,
249
+ tokenEndpoint,
250
+ scopes,
251
+ scopeString: scopes.join(" "),
252
+ tokenAuthMethod: chooseTokenAuthMethod(
253
+ stringList(metadata.token_endpoint_auth_methods_supported)
254
+ ),
255
+ ...registrationEndpoint === void 0 ? {} : { registrationEndpoint },
256
+ ...revocationEndpoint === void 0 ? {} : { revocationEndpoint }
257
+ });
258
+ }
259
+ function protectedResourceCandidates(endpoint) {
260
+ const url = new URL(endpoint);
261
+ const path = url.pathname.replace(/\/+$/u, "");
262
+ return [
263
+ `${url.origin}/.well-known/oauth-protected-resource${path}`,
264
+ `${url.origin}/.well-known/oauth-protected-resource`
265
+ ];
266
+ }
267
+ function authorizationServerCandidates(issuer) {
268
+ const url = new URL(issuer);
269
+ const path = url.pathname.replace(/\/+$/u, "");
270
+ const bare = issuer.replace(/\/+$/u, "");
271
+ return [
272
+ `${url.origin}/.well-known/oauth-authorization-server${path}`,
273
+ `${bare}/.well-known/oauth-authorization-server`,
274
+ `${url.origin}/.well-known/openid-configuration${path}`,
275
+ `${bare}/.well-known/openid-configuration`
276
+ ];
277
+ }
278
+ function chooseScopes(profile, advertised) {
279
+ const wanted = profile.oauth.scopes;
280
+ if (advertised === void 0 || advertised.length === 0)
281
+ return [...profile.oauth.fallbackScopes];
282
+ const chosen = wanted.filter((scope) => advertised.includes(scope));
283
+ if (chosen.length > 0) return chosen;
284
+ return advertised.includes("offline_access") ? ["offline_access"] : [...profile.oauth.fallbackScopes];
285
+ }
286
+ function stringList(value) {
287
+ if (!Array.isArray(value)) return void 0;
288
+ const items = value.filter(
289
+ (item) => typeof item === "string"
290
+ );
291
+ return items.length > 0 ? items : void 0;
292
+ }
293
+ function firstString(value) {
294
+ return typeof value === "string" && value !== "" ? value : void 0;
295
+ }
296
+
297
+ // ../mcp-oauth/src/provider.ts
298
+ function needsOAuth(profile) {
299
+ return profile.oauth !== void 0;
300
+ }
301
+ function credentialKeyOf(profile) {
302
+ return credentialKeyFor(profile.scope, profile.instance);
303
+ }
304
+ function accountLabel(profile) {
305
+ return profile.instance === DEFAULT_INSTANCE ? profile.scope : `${profile.scope}/${profile.instance}`;
306
+ }
307
+
308
+ // ../mcp-oauth/src/tool-name.ts
309
+ var MAX_TOOL_NAME = 64;
310
+ function toolPrefix(namespace) {
311
+ return `mcp__${namespace}__`;
312
+ }
313
+ function publicToolName(serverTool, namespace) {
314
+ const cleaned = serverTool.replace(/[^A-Za-z0-9_-]/gu, "_") || "tool";
315
+ const full = `${toolPrefix(namespace)}${cleaned}`;
316
+ if (full.length <= MAX_TOOL_NAME) return full;
317
+ const digest = digestOf(serverTool);
318
+ return `${full.slice(0, MAX_TOOL_NAME - digest.length - 1)}_${digest}`;
319
+ }
320
+ function digestOf(input) {
321
+ let hash = 0xcbf29ce484222325n;
322
+ const prime = 0x100000001b3n;
323
+ const mask = 0xffffffffffffffffn;
324
+ for (let index = 0; index < input.length; index += 1) {
325
+ hash = (hash ^ BigInt(input.charCodeAt(index))) * prime & mask;
326
+ }
327
+ return hash.toString(16).padStart(16, "0").slice(0, 12);
328
+ }
329
+
330
+ // ../mcp-oauth/src/bridge.ts
331
+ var TOOL_CALL_TIMEOUT_MS = 6e4;
332
+ var McpBridge = class {
333
+ constructor(options) {
334
+ this.options = options;
335
+ if (needsOAuth(options.profile) && options.store === void 0)
336
+ throw new TypeError(
337
+ `the ${accountLabel(options.profile)} bridge needs a grant store for its OAuth profile`
338
+ );
339
+ }
340
+ client;
341
+ oauth;
342
+ disposers = /* @__PURE__ */ new Map();
343
+ names = [];
344
+ ready = false;
345
+ failure;
346
+ /** Public names of the published tools, in discovery order. */
347
+ get toolNames() {
348
+ return this.names;
349
+ }
350
+ /** Whether the connection is currently usable. */
351
+ get isConnected() {
352
+ return this.ready;
353
+ }
354
+ /**
355
+ * Why the last connection attempt failed, when one did.
356
+ *
357
+ * A server that refuses the connection — a resource that needs an
358
+ * administrator's switch, a scope the token lacks — says so here, so a
359
+ * settings card can show the reason instead of an unexplained "not connected".
360
+ */
361
+ get lastFailure() {
362
+ return this.failure;
363
+ }
364
+ /**
365
+ * Connect and publish tools, once the account is signed in if it needs to be.
366
+ * @param ctx - host context, for logging.
367
+ * @returns whether the bridge is usable; failures are logged, never thrown.
368
+ */
369
+ async ensure(ctx) {
370
+ const { profile, store } = this.options;
371
+ if (needsOAuth(profile)) {
372
+ const grant = await store?.read();
373
+ if (grant === void 0 || !grant.isSignedIn()) return false;
374
+ }
375
+ try {
376
+ const oauth = needsOAuth(profile) ? await this.authorizationServer(profile) : void 0;
377
+ if (this.client === void 0) await this.connect(profile, store, oauth);
378
+ await this.publishTools(ctx);
379
+ await this.options.onConnected?.();
380
+ this.failure = void 0;
381
+ loggerOf(ctx).info(
382
+ "[%s] %d tools available from %s",
383
+ accountLabel(profile),
384
+ this.names.length,
385
+ profile.endpoint
386
+ );
387
+ return true;
388
+ } catch (error) {
389
+ this.ready = false;
390
+ await this.drop();
391
+ this.failure = errorText(error);
392
+ loggerOf(ctx).warn(
393
+ "[%s] MCP bridge unavailable: %s",
394
+ accountLabel(profile),
395
+ this.failure
396
+ );
397
+ return false;
398
+ }
399
+ }
400
+ /** Close the connection and withdraw every published tool. */
401
+ async drop() {
402
+ for (const dispose of this.disposers.values()) dispose();
403
+ this.disposers = /* @__PURE__ */ new Map();
404
+ this.names = [];
405
+ this.ready = false;
406
+ this.failure = void 0;
407
+ const client = this.client;
408
+ this.client = void 0;
409
+ if (client !== void 0) await client.close().catch(() => void 0);
410
+ }
411
+ /**
412
+ * Call one tool of the connected server by its own name.
413
+ * @param name - the server's tool name, without any published prefix.
414
+ * @param args - tool arguments.
415
+ * @throws when the connection is not open.
416
+ */
417
+ async callTool(name2, args) {
418
+ const client = this.client;
419
+ if (client === void 0)
420
+ throw new Error(
421
+ `the ${this.options.profile.label} MCP connection is not open`
422
+ );
423
+ return await client.callTool(
424
+ { name: name2, arguments: args },
425
+ { timeout: TOOL_CALL_TIMEOUT_MS }
426
+ );
427
+ }
428
+ /** The discovered authorization server, remembered across reconnects. */
429
+ async authorizationServer(profile) {
430
+ const oauth = this.oauth ?? await discover(profile);
431
+ this.oauth = oauth;
432
+ return oauth;
433
+ }
434
+ /** Open the MCP connection, authenticated when the profile has OAuth facts. */
435
+ async connect(profile, store, oauth) {
436
+ const { Client: McpClient, StreamableHTTPClientTransport } = await import("@modelcontextprotocol/client");
437
+ const transport = new StreamableHTTPClientTransport(
438
+ new URL(profile.endpoint),
439
+ oauth === void 0 || store === void 0 ? {} : {
440
+ authProvider: {
441
+ // Asked for before every request, so a refreshed token needs no reconnect.
442
+ token: async () => await store.accessToken(oauth),
443
+ onUnauthorized: async () => {
444
+ await store.refresh(oauth);
445
+ }
446
+ }
447
+ }
448
+ );
449
+ const client = new McpClient(this.options.clientInfo, { capabilities: {} });
450
+ await client.connect(transport);
451
+ this.client = client;
452
+ this.ready = true;
453
+ }
454
+ /** Publish every server tool, replacing the previous generation. */
455
+ async publishTools(ctx) {
456
+ const client = this.client;
457
+ if (client === void 0) throw new Error("the MCP connection is not open");
458
+ const { createMcpToolDefinition } = await import("@deepseek-ai/dsh-mcp-client");
459
+ const listed = await client.listTools(void 0, { cacheMode: "refresh" });
460
+ const disposers = /* @__PURE__ */ new Map();
461
+ 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();
493
+ this.disposers = disposers;
494
+ this.names = names;
495
+ return names;
496
+ }
497
+ };
498
+
499
+ // ../mcp-oauth/src/browser.ts
500
+ import { execFile } from "node:child_process";
501
+ 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];
504
+ execFile(command, args, (error) => {
505
+ if (error !== null)
506
+ loggerOf(ctx).warn(
507
+ "[mcp-oauth] could not open a browser: %s",
508
+ errorText(error)
509
+ );
510
+ });
511
+ }
512
+
513
+ // ../mcp-oauth/src/flow.ts
514
+ import { createHash, randomBytes } from "node:crypto";
515
+
516
+ // ../mcp-oauth/src/json.ts
517
+ function jsonSafe(value) {
518
+ if (Array.isArray(value)) {
519
+ const items = [];
520
+ for (const item of value) {
521
+ const safe = jsonSafe(item);
522
+ if (safe !== void 0) items.push(safe);
523
+ }
524
+ return items;
525
+ }
526
+ if (value !== null && typeof value === "object") {
527
+ const out = {};
528
+ for (const [key, item] of Object.entries(value)) {
529
+ const safe = jsonSafe(item);
530
+ if (safe !== void 0) out[key] = safe;
531
+ }
532
+ return out;
533
+ }
534
+ if (value === void 0 || typeof value === "function" || typeof value === "symbol")
535
+ return void 0;
536
+ if (typeof value === "number" && !Number.isFinite(value)) return void 0;
537
+ return value;
538
+ }
539
+
540
+ // ../mcp-oauth/src/grant.ts
541
+ var Grant = class _Grant {
542
+ version;
543
+ /** Issuer the tokens came from, so a generation switch is detectable. */
544
+ issuer;
545
+ /** MCP resource the tokens are valid for (RFC 8707). */
546
+ resource;
547
+ /** Dynamically registered client identity. */
548
+ clientId;
549
+ /** Present only for a server that issues one, which a confidential client requires. */
550
+ clientSecret;
551
+ /** How this client authenticates at the token endpoint. */
552
+ tokenAuthMethod;
553
+ /** The exact redirect URI the client was registered with. */
554
+ redirectUri;
555
+ /** Application data the provider keeps with its tokens. */
556
+ extra;
557
+ accessToken;
558
+ refreshToken;
559
+ tokenType;
560
+ scope;
561
+ expiresAt;
562
+ constructor(params) {
563
+ this.version = params.version;
564
+ this.issuer = params.issuer;
565
+ this.resource = params.resource;
566
+ this.clientId = params.clientId;
567
+ this.clientSecret = params.clientSecret;
568
+ this.tokenAuthMethod = params.tokenAuthMethod;
569
+ this.redirectUri = params.redirectUri;
570
+ this.extra = params.extra;
571
+ this.accessToken = params.accessToken;
572
+ this.refreshToken = params.refreshToken;
573
+ this.tokenType = params.tokenType;
574
+ this.scope = params.scope;
575
+ this.expiresAt = params.expiresAt;
576
+ }
577
+ /**
578
+ * Rebuild a grant from a stored payload.
579
+ * @param payload - whatever the credential store returned.
580
+ * @returns the grant, or undefined for a payload that is not an object.
581
+ */
582
+ static from(payload) {
583
+ if (payload === null || typeof payload !== "object") return void 0;
584
+ return new _Grant(payload);
585
+ }
586
+ /** Whether a stored grant carries a usable access token. */
587
+ isSignedIn() {
588
+ return typeof this.accessToken === "string" && this.accessToken !== "";
589
+ }
590
+ };
591
+ var GrantStore = class {
592
+ /**
593
+ * @param ctx - host context carrying the credential store.
594
+ * @param key - the record key, in the store's `<scope>/<id>` grammar.
595
+ */
596
+ constructor(ctx, key) {
597
+ this.ctx = ctx;
598
+ this.credentialKey = key;
599
+ }
600
+ /** The record key, as the harness authorization seam spells it. */
601
+ credentialKey;
602
+ /** Read the stored grant, or undefined when there is none. */
603
+ async read() {
604
+ const record = await this.ctx.credentials.readRecord(this.credentialKey);
605
+ if (record === void 0 || record.kind !== "grant") return void 0;
606
+ return Grant.from(record.payload);
607
+ }
608
+ /**
609
+ * Read-decide-replace the grant under the store's cross-process lock.
610
+ * @param mutate - receives the current grant; returning undefined leaves it alone.
611
+ * @returns the stored grant after the change.
612
+ */
613
+ async mutate(mutate) {
614
+ const record = await this.ctx.credentials.modifyRecord(
615
+ this.credentialKey,
616
+ async (current) => {
617
+ const previous = current?.kind === "grant" ? Grant.from(current.payload) : void 0;
618
+ const next = await mutate(previous);
619
+ if (next === void 0) return void 0;
620
+ const payload = jsonSafe(next);
621
+ if (payload === void 0) return void 0;
622
+ return { kind: "grant", payload };
623
+ }
624
+ );
625
+ return record?.kind === "grant" ? Grant.from(record.payload) : void 0;
626
+ }
627
+ /** Merge provider facts into the record, creating it when absent. */
628
+ async remember(patch) {
629
+ await this.mutate(
630
+ async (grant) => new Grant({
631
+ ...grant ?? { version: 1 },
632
+ version: 1,
633
+ extra: { ...grant?.extra, ...patch }
634
+ })
635
+ );
636
+ }
637
+ /** Forget the record entirely; the issuer is not told, matching the seam. */
638
+ async clear() {
639
+ await this.ctx.credentials.deleteRecord(this.credentialKey);
640
+ }
641
+ /**
642
+ * The token an MCP transport asks for before every request, refreshed when
643
+ * it is about to expire.
644
+ * @param oauth - the authorization server the tokens belong to.
645
+ * @returns the access token, or undefined when nobody is signed in.
646
+ */
647
+ async accessToken(oauth) {
648
+ const grant = await this.read();
649
+ if (grant === void 0 || !grant.isSignedIn()) return void 0;
650
+ if (Date.now() < (grant.expiresAt ?? 0) - REFRESH_MARGIN_MS)
651
+ return grant.accessToken;
652
+ return await this.refresh(oauth);
653
+ }
654
+ /**
655
+ * Exchange the refresh token for a new access token, inside the store lock.
656
+ * A peer that refreshed while we waited makes this a no-op.
657
+ * @param oauth - the authorization server to refresh against.
658
+ * @throws when the record carries no refresh token, or nobody is signed in.
659
+ */
660
+ async refresh(oauth) {
661
+ const after = await this.mutate(async (grant) => {
662
+ if (grant === void 0) return void 0;
663
+ if (Date.now() < (grant.expiresAt ?? 0) - REFRESH_MARGIN_MS)
664
+ return void 0;
665
+ if (typeof grant.refreshToken !== "string" || grant.refreshToken === "")
666
+ throw new Error(
667
+ "the stored sign-in has no refresh token; sign in again"
668
+ );
669
+ const payload = await oauth.tokenRequest(grant, {
670
+ grant_type: "refresh_token",
671
+ refresh_token: grant.refreshToken
672
+ });
673
+ return new Grant({ ...grant, ...oauth.toTokenFields(payload, grant) });
674
+ });
675
+ if (after === void 0 || !after.isSignedIn())
676
+ throw new Error("not signed in");
677
+ return after.accessToken;
678
+ }
679
+ };
680
+
681
+ // ../mcp-oauth/src/loopback.ts
682
+ import { timingSafeEqual } from "node:crypto";
683
+ import { createServer } from "node:http";
684
+ var LoopbackListener = class _LoopbackListener {
685
+ server;
686
+ port;
687
+ redirectUri;
688
+ /** Resolves with the authorization code the browser returned. */
689
+ code;
690
+ fail;
691
+ constructor(server, port, redirectUri, code, fail) {
692
+ this.server = server;
693
+ this.port = port;
694
+ this.redirectUri = redirectUri;
695
+ this.code = code;
696
+ this.fail = fail;
697
+ }
698
+ /**
699
+ * Bind the first free port at or after `preferredPort` and wait for the code.
700
+ * @param preferredPort - the configured port to try first.
701
+ * @param expectedState - the `state` value the response must carry back.
702
+ * @param label - the provider's name, for the pages the browser sees.
703
+ */
704
+ static async listenForCode(preferredPort, expectedState, label) {
705
+ let resolveCode = () => {
706
+ };
707
+ let rejectCode = () => {
708
+ };
709
+ const code = new Promise((resolve, reject) => {
710
+ resolveCode = resolve;
711
+ rejectCode = reject;
712
+ });
713
+ code.catch(() => void 0);
714
+ let lastError;
715
+ for (let offset = 0; offset < PORT_ATTEMPTS; offset += 1) {
716
+ const port = preferredPort + offset;
717
+ const server = createServer((request, response) => {
718
+ const url = new URL(request.url ?? "/", `http://127.0.0.1:${port}`);
719
+ if (url.pathname !== "/callback") {
720
+ response.writeHead(404, { "content-type": "text/plain; charset=utf-8" }).end("not found");
721
+ return;
722
+ }
723
+ const gotState = url.searchParams.get("state") ?? "";
724
+ const oauthError = url.searchParams.get("error");
725
+ const returned = url.searchParams.get("code");
726
+ const comparable = Buffer.byteLength(gotState) === Buffer.byteLength(expectedState);
727
+ const stateMatches = comparable && timingSafeEqual(Buffer.from(gotState), Buffer.from(expectedState));
728
+ if (oauthError !== null || returned === null || !stateMatches) {
729
+ const reason = oauthError !== null ? `${label} reported: ${oauthError}` : "The response did not match this sign-in attempt.";
730
+ response.writeHead(400, { "content-type": "text/html; charset=utf-8" }).end(_LoopbackListener.callbackPage("Authorization failed", reason));
731
+ rejectCode(
732
+ new Error(
733
+ oauthError !== null ? `authorization denied: ${oauthError}` : "state mismatch"
734
+ )
735
+ );
736
+ return;
737
+ }
738
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" }).end(
739
+ _LoopbackListener.callbackPage(
740
+ `Signed in to ${label}`,
741
+ "You can close this tab and return to DeepSeek Harness."
742
+ )
743
+ );
744
+ resolveCode(returned);
745
+ });
746
+ try {
747
+ await new Promise((resolve, reject) => {
748
+ server.once("error", reject);
749
+ server.listen(port, "127.0.0.1", resolve);
750
+ });
751
+ } catch (error) {
752
+ lastError = error;
753
+ continue;
754
+ }
755
+ return new _LoopbackListener(
756
+ server,
757
+ port,
758
+ `http://127.0.0.1:${port}/callback`,
759
+ code,
760
+ rejectCode
761
+ );
762
+ }
763
+ throw new Error(
764
+ `no free loopback port in ${preferredPort}..${preferredPort + PORT_ATTEMPTS - 1}: ${errorText(lastError)}`
765
+ );
766
+ }
767
+ // `close()` alone only stops accepting: it waits for existing sockets,
768
+ // and a browser that used keep-alive would hold the flow open forever.
769
+ close = async () => {
770
+ this.server.closeAllConnections();
771
+ await new Promise((resolve) => {
772
+ this.server.close(() => resolve());
773
+ });
774
+ };
775
+ 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>`;
777
+ };
778
+ };
779
+
780
+ // ../mcp-oauth/src/flow.ts
781
+ var AuthorizationFlow = class {
782
+ /**
783
+ * @param profile - the provider account to sign in to.
784
+ * @param store - the record the finished grant is committed through.
785
+ */
786
+ constructor(profile, store) {
787
+ this.profile = profile;
788
+ this.store = store;
789
+ }
790
+ /**
791
+ * Run one sign-in attempt to completion.
792
+ * @param _ctx - host context; the flow itself only touches the store.
793
+ * @param session - the seam's session: signals cancellation, publishes notices, commits.
794
+ */
795
+ async run(_ctx, session) {
796
+ const endpoint = this.profile.endpoint;
797
+ const oauth = await discover(this.profile);
798
+ const verifier = b64url(randomBytes(32));
799
+ const expectedState = b64url(randomBytes(32));
800
+ const challenge = b64url(createHash("sha256").update(verifier).digest());
801
+ const listener = await LoopbackListener.listenForCode(
802
+ this.profile.oauth.redirectPort,
803
+ expectedState,
804
+ this.profile.label
805
+ );
806
+ const onAbort = () => listener.fail(new Error("sign-in cancelled"));
807
+ session.signal.addEventListener("abort", onAbort, { once: true });
808
+ let timer;
809
+ try {
810
+ const existing = await this.store.read();
811
+ const client = await this.clientFor(
812
+ oauth,
813
+ listener.redirectUri,
814
+ existing
815
+ );
816
+ const authorize = new URL(oauth.authorizationEndpoint);
817
+ for (const [key, value] of Object.entries({
818
+ response_type: "code",
819
+ client_id: client.clientId,
820
+ redirect_uri: listener.redirectUri,
821
+ scope: oauth.scopeString,
822
+ state: expectedState,
823
+ code_challenge: challenge,
824
+ code_challenge_method: "S256",
825
+ resource: endpoint
826
+ })) {
827
+ authorize.searchParams.set(key, value);
828
+ }
829
+ session.notify({
830
+ message: `Approve the ${this.profile.label} sign-in in your browser.`,
831
+ url: authorize.href
832
+ });
833
+ const authorizationCode = await new Promise((resolve, reject) => {
834
+ timer = setTimeout(
835
+ () => reject(new Error("sign-in timed out")),
836
+ SIGN_IN_TIMEOUT_MS
837
+ );
838
+ listener.code.then(resolve, reject);
839
+ });
840
+ session.signal.throwIfAborted();
841
+ const payload = await oauth.tokenRequest(client, {
842
+ grant_type: "authorization_code",
843
+ code: authorizationCode,
844
+ redirect_uri: listener.redirectUri,
845
+ code_verifier: verifier
846
+ });
847
+ const base = {
848
+ ...existing ?? { version: 1 },
849
+ version: 1,
850
+ issuer: oauth.issuer,
851
+ resource: endpoint,
852
+ redirectUri: listener.redirectUri,
853
+ ...client,
854
+ scope: oauth.scopeString
855
+ };
856
+ const payloadPair = jsonSafe({
857
+ ...base,
858
+ ...oauth.toTokenFields(payload, base)
859
+ });
860
+ if (payloadPair === void 0)
861
+ throw new Error("the grant could not be represented as JSON");
862
+ await session.commit({ kind: "grant", payload: payloadPair });
863
+ } finally {
864
+ clearTimeout(timer);
865
+ session.signal.removeEventListener("abort", onAbort);
866
+ await listener.close();
867
+ }
868
+ }
869
+ /**
870
+ * The client to authorize with, registering one when the stored identity
871
+ * cannot be reused.
872
+ *
873
+ * A registration is bound to the exact redirect URIs it was issued for, and
874
+ * the loopback port is not guaranteed to be free, so a stored client is
875
+ * reused only when the resource and the URI both still match.
876
+ */
877
+ async clientFor(oauth, redirectUri, existing) {
878
+ if (existing !== void 0 && existing.resource === this.profile.endpoint && existing.redirectUri === redirectUri && typeof existing.clientId === "string") {
879
+ const tokenAuthMethod = authMethodOf(existing);
880
+ return existing.clientSecret === void 0 ? { clientId: existing.clientId, tokenAuthMethod } : {
881
+ clientId: existing.clientId,
882
+ clientSecret: existing.clientSecret,
883
+ tokenAuthMethod
884
+ };
885
+ }
886
+ const registered = await oauth.registerClient(
887
+ redirectUri,
888
+ this.profile.oauth.clientName
889
+ );
890
+ await this.store.mutate(
891
+ async (grant) => new Grant({
892
+ ...grant ?? { version: 1 },
893
+ version: 1,
894
+ issuer: oauth.issuer,
895
+ resource: this.profile.endpoint,
896
+ redirectUri,
897
+ ...registered
898
+ })
899
+ );
900
+ return registered;
901
+ }
902
+ };
903
+ function b64url(input) {
904
+ return input.toString("base64").replace(/\+/gu, "-").replace(/\//gu, "_").replace(/=+$/u, "");
905
+ }
906
+
907
+ // ../mcp-oauth/src/payload.ts
908
+ function mcpText(value) {
909
+ if (value === null || typeof value !== "object") return [];
910
+ const content = value.content;
911
+ if (!Array.isArray(content)) return [];
912
+ const out = [];
913
+ for (const block of content) {
914
+ if (block === null || typeof block !== "object") continue;
915
+ const { type, text } = block;
916
+ if (type === "text" && typeof text === "string") out.push(text);
917
+ }
918
+ return out;
919
+ }
920
+ function mcpJson(value) {
921
+ const roots = [];
922
+ if (value !== null && typeof value === "object") {
923
+ const structured = value.structuredContent;
924
+ if (structured !== void 0) roots.push(structured);
925
+ }
926
+ for (const text of mcpText(value)) {
927
+ const trimmed = text.trim();
928
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) continue;
929
+ try {
930
+ roots.push(JSON.parse(trimmed));
931
+ } catch {
932
+ }
933
+ }
934
+ return roots;
935
+ }
936
+ function asRecord(value) {
937
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
938
+ }
939
+ function pickString(source, ...names) {
940
+ const record = asRecord(source);
941
+ if (record === void 0) return void 0;
942
+ for (const name2 of names) {
943
+ const value = record[name2];
944
+ if (typeof value === "string" && value.trim() !== "") return value.trim();
945
+ if (typeof value === "number") return String(value);
946
+ }
947
+ return void 0;
948
+ }
949
+ function pickNumber(source, ...names) {
950
+ const record = asRecord(source);
951
+ if (record === void 0) return void 0;
952
+ for (const name2 of names) {
953
+ const value = record[name2];
954
+ if (typeof value === "number" && Number.isFinite(value)) return value;
955
+ if (typeof value === "string" && value.trim() !== "") {
956
+ const parsed = Number(value);
957
+ if (Number.isFinite(parsed)) return parsed;
958
+ }
959
+ }
960
+ return void 0;
961
+ }
962
+
963
+ // ../integration-host/src/auth.ts
964
+ var IntegrationAuth = class {
965
+ constructor(deps) {
966
+ this.deps = deps;
967
+ }
968
+ /** Start a sign-in attempt, or join the one already running. */
969
+ async signIn() {
970
+ const { ctx, state, spec } = this.deps;
971
+ if (state.running !== void 0) return state.running;
972
+ state.notice = void 0;
973
+ state.error = void 0;
974
+ const controller = new AbortController();
975
+ state.running = (async () => {
976
+ try {
977
+ const outcome = await ctx.authorization.begin({
978
+ key: this.deps.store.credentialKey,
979
+ signal: controller.signal,
980
+ interaction: {
981
+ // The flow publishes the authorize URL; opening it is the driver's
982
+ // business, because a generic UI driving the same flow would render
983
+ // the URL instead of launching a browser.
984
+ notify: (notice) => {
985
+ state.notice = notice;
986
+ if (typeof notice.url === "string") openBrowser(ctx, notice.url);
987
+ },
988
+ prompt: () => Promise.reject(
989
+ new Error(`${spec.label} sign-in needs no prompts`)
990
+ )
991
+ }
992
+ });
993
+ if (outcome.status === "authorized") {
994
+ state.notice = { message: `Signed in to ${spec.label}.` };
995
+ await this.connect();
996
+ } else {
997
+ state.error = "Sign-in was cancelled.";
998
+ }
999
+ return outcome;
1000
+ } catch (error) {
1001
+ state.error = errorText(error);
1002
+ loggerOf(ctx).warn("%s sign-in failed: %s", spec.logTag, state.error);
1003
+ return { status: "failed" };
1004
+ } finally {
1005
+ state.running = void 0;
1006
+ }
1007
+ })();
1008
+ return state.running;
1009
+ }
1010
+ /** Forget the sign-in and withdraw every published tool. */
1011
+ async signOut() {
1012
+ const { ctx, state, store, bridge } = this.deps;
1013
+ try {
1014
+ ctx.authorization.cancel(store.credentialKey);
1015
+ } catch {
1016
+ }
1017
+ await bridge.drop();
1018
+ await store.clear();
1019
+ state.notice = void 0;
1020
+ state.error = void 0;
1021
+ state.facts = void 0;
1022
+ }
1023
+ /**
1024
+ * Connect, restoring the known account facts first so published tools have
1025
+ * them.
1026
+ * @returns whether the connection is usable.
1027
+ */
1028
+ async connect() {
1029
+ const { state, store, bridge, ctx, spec } = this.deps;
1030
+ state.facts = spec.facts.from(await store.read());
1031
+ const ready = await bridge.ensure(ctx);
1032
+ if (ready) {
1033
+ state.error = void 0;
1034
+ await this.learn();
1035
+ } else {
1036
+ state.error = bridge.lastFailure;
1037
+ }
1038
+ return ready;
1039
+ }
1040
+ /** Build the status document for one caller. */
1041
+ async getStatus() {
1042
+ const { state, config, bridge, store, spec } = this.deps;
1043
+ const grant = await store.read();
1044
+ const signedIn = grant?.isSignedIn() ?? false;
1045
+ return spec.status(
1046
+ {
1047
+ signedIn,
1048
+ inFlight: state.running !== void 0,
1049
+ expiresAt: signedIn ? grant?.expiresAt ?? null : null,
1050
+ scope: signedIn ? grant?.scope ?? null : null,
1051
+ notice: state.notice ?? null,
1052
+ error: state.error ?? null,
1053
+ connected: bridge.isConnected,
1054
+ tools: [...bridge.toolNames],
1055
+ endpoint: config.endpoint
1056
+ },
1057
+ state.facts ?? spec.facts.from(grant)
1058
+ );
1059
+ }
1060
+ /** The line the command and any notice show for a sign-in in progress. */
1061
+ loginMessage(status) {
1062
+ if (status.notice?.url !== void 0)
1063
+ return `${status.notice.message} ${status.notice.url}`;
1064
+ return status.notice?.message ?? `Opening your browser to sign in to ${this.deps.spec.label}\u2026`;
1065
+ }
1066
+ /** Wait briefly so a just-started attempt can publish its authorize URL. */
1067
+ async settleNotice(ms = 400) {
1068
+ await new Promise((resolve) => {
1069
+ setTimeout(resolve, ms);
1070
+ });
1071
+ }
1072
+ /** Learn the account facts once, and remember them for the next start. */
1073
+ async learn() {
1074
+ const { ctx, state, store, bridge, spec } = this.deps;
1075
+ const known = state.facts;
1076
+ if (known?.isComplete === true) return;
1077
+ if (!bridge.toolNames.some((name2) => name2.endsWith(spec.facts.toolName)))
1078
+ return;
1079
+ try {
1080
+ const learned = spec.facts.parse(
1081
+ await bridge.callTool(spec.facts.toolName, {})
1082
+ );
1083
+ if (learned.isEmpty) return;
1084
+ const merged = spec.facts.merge(known, learned);
1085
+ state.facts = merged;
1086
+ await store.remember(spec.facts.remember(merged));
1087
+ } catch (error) {
1088
+ loggerOf(ctx).warn(
1089
+ "%s could not read the account facts: %s",
1090
+ spec.logTag,
1091
+ errorText(error)
1092
+ );
1093
+ }
1094
+ }
1095
+ };
1096
+
1097
+ // ../integration-host/src/command.ts
1098
+ var ACTIONS = ["status", "login", "logout", "sync"];
1099
+ var createIntegrationCommand = (spec, auth) => {
1100
+ const parseAction = (rawInput) => {
1101
+ const first = String(rawInput ?? "").trim().split(/\s+/u)[0] ?? "";
1102
+ return ACTIONS.includes(first) ? first : "status";
1103
+ };
1104
+ return async ({ rawInput }) => {
1105
+ const action = parseAction(rawInput);
1106
+ try {
1107
+ if (action === "login") {
1108
+ void auth.signIn();
1109
+ await auth.settleNotice();
1110
+ return {
1111
+ kind: "success",
1112
+ text: auth.loginMessage(await auth.getStatus())
1113
+ };
1114
+ }
1115
+ if (action === "logout") {
1116
+ await auth.signOut();
1117
+ return { kind: "success", text: `Signed out of ${spec.label}.` };
1118
+ }
1119
+ if (action === "sync") {
1120
+ const ok = await auth.connect();
1121
+ const status2 = await auth.getStatus();
1122
+ return ok ? {
1123
+ kind: "success",
1124
+ text: `${spec.label} tools ready (${status2.tools.length}).`
1125
+ } : {
1126
+ kind: "error",
1127
+ text: status2.error ?? `Not signed in to ${spec.label}. Run /${spec.name} login.`
1128
+ };
1129
+ }
1130
+ const status = await auth.getStatus();
1131
+ if (!status.signedIn)
1132
+ return {
1133
+ kind: "success",
1134
+ text: `Not signed in to ${spec.label}. Run /${spec.name} login.`
1135
+ };
1136
+ const until = status.expiresAt === null ? "unknown" : new Date(status.expiresAt).toLocaleString();
1137
+ const extra = spec.describe?.(status) ?? "";
1138
+ const tail = extra === "" ? "" : ` ${extra}`;
1139
+ return {
1140
+ kind: "success",
1141
+ text: `Signed in to ${spec.label}. Token valid until ${until}.${tail} ${status.tools.length} tools loaded.`
1142
+ };
1143
+ } catch (error) {
1144
+ return { kind: "error", text: `${spec.label}: ${errorText(error)}` };
1145
+ }
1146
+ };
1147
+ };
1148
+
1149
+ // ../integration-host/src/config.ts
1150
+ function resolveEndpointConfig(input, defaults) {
1151
+ const endpoint = typeof input?.endpoint === "string" && input.endpoint !== "" ? input.endpoint : defaults.endpoint;
1152
+ const port = Number(input?.redirectPort);
1153
+ return {
1154
+ endpoint,
1155
+ redirectPort: Number.isInteger(port) && port > 0 && port < 65536 ? port : defaults.redirectPort,
1156
+ debug: input?.debug === true
1157
+ };
1158
+ }
1159
+
1160
+ // ../integration-host/src/correlation.ts
1161
+ import { Service } from "@deepseek-ai/cordis";
1162
+ var CORRELATION_SOURCES = "correlationSources";
1163
+
1164
+ // ../integration-host/src/digest.ts
1165
+ import { Service as Service2 } from "@deepseek-ai/cordis";
1166
+ var DIGEST_SOURCES = "digestSources";
1167
+
1168
+ // ../integration-host/src/markdown.ts
1169
+ var FENCE = /^\s{0,3}(?:```|~~~)/u;
1170
+ var OPEN = "<!--";
1171
+ var CLOSE = "-->";
1172
+ var uncomment = (lines) => {
1173
+ const kept = [];
1174
+ let inFence = false;
1175
+ let inComment = false;
1176
+ for (const line of lines) {
1177
+ if (!inComment && FENCE.test(line)) {
1178
+ inFence = !inFence;
1179
+ kept.push({ text: line, touched: false });
1180
+ continue;
1181
+ }
1182
+ if (inFence) {
1183
+ kept.push({ text: line, touched: false });
1184
+ continue;
1185
+ }
1186
+ let rest = line;
1187
+ let text = "";
1188
+ let touched = false;
1189
+ while (rest !== "") {
1190
+ if (inComment) {
1191
+ const end = rest.indexOf(CLOSE);
1192
+ if (end === -1) {
1193
+ rest = "";
1194
+ break;
1195
+ }
1196
+ inComment = false;
1197
+ rest = rest.slice(end + CLOSE.length);
1198
+ continue;
1199
+ }
1200
+ const start = rest.indexOf(OPEN);
1201
+ if (start === -1) {
1202
+ text += rest;
1203
+ rest = "";
1204
+ break;
1205
+ }
1206
+ text += rest.slice(0, start);
1207
+ touched = true;
1208
+ inComment = true;
1209
+ rest = rest.slice(start + OPEN.length);
1210
+ }
1211
+ kept.push({ text, touched });
1212
+ }
1213
+ return kept;
1214
+ };
1215
+ var stripHtmlComments = (markdown) => {
1216
+ if (!markdown.includes(OPEN)) return markdown;
1217
+ const kept = [];
1218
+ let inFence = false;
1219
+ let blank = false;
1220
+ for (const line of uncomment(markdown.split("\n"))) {
1221
+ if (FENCE.test(line.text)) {
1222
+ inFence = !inFence;
1223
+ blank = false;
1224
+ kept.push(line.text);
1225
+ continue;
1226
+ }
1227
+ if (inFence) {
1228
+ kept.push(line.text);
1229
+ continue;
1230
+ }
1231
+ if (line.text.trim() === "") {
1232
+ if (kept.length === 0 || blank) continue;
1233
+ blank = true;
1234
+ kept.push(line.text);
1235
+ continue;
1236
+ }
1237
+ blank = false;
1238
+ kept.push(
1239
+ line.touched ? line.text.replace(/[ \t]{2,}(?=\S)/gu, " ") : line.text
1240
+ );
1241
+ }
1242
+ while (kept.length > 0 && kept[kept.length - 1]?.trim() === "") kept.pop();
1243
+ return kept.join("\n");
1244
+ };
1245
+
1246
+ // ../integration-host/src/route.ts
1247
+ var MAX_BODY_BYTES = 8 * 1024;
1248
+ function mountIntegrationRoute(ctx, spec) {
1249
+ ctx.inject(["webServer"], (scoped) => {
1250
+ const webServer = scoped.get(
1251
+ "webServer"
1252
+ );
1253
+ scoped.effect(
1254
+ () => webServer.register({
1255
+ kind: "prefix",
1256
+ path: spec.prefix,
1257
+ handler: spec.handler
1258
+ }),
1259
+ `${spec.prefix}: route`
1260
+ );
1261
+ });
1262
+ }
1263
+ var BadRequest = class extends Error {
1264
+ };
1265
+ function bodyOf(value) {
1266
+ return {
1267
+ fields: value,
1268
+ required: (name2) => {
1269
+ const field = value?.[name2];
1270
+ if (typeof field !== "string" || field === "")
1271
+ throw new BadRequest(`the request needs a "${name2}"`);
1272
+ return field;
1273
+ }
1274
+ };
1275
+ }
1276
+ async function readJson(request) {
1277
+ const chunks = [];
1278
+ let size = 0;
1279
+ for await (const chunk of request) {
1280
+ const buffer = chunk;
1281
+ size += buffer.length;
1282
+ if (size > MAX_BODY_BYTES)
1283
+ throw new Error("request body is larger than this route accepts");
1284
+ chunks.push(buffer);
1285
+ }
1286
+ if (chunks.length === 0) return void 0;
1287
+ try {
1288
+ const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
1289
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
1290
+ } catch {
1291
+ return void 0;
1292
+ }
1293
+ }
1294
+ function createIntegrationRoute(spec) {
1295
+ const { prefix, auth } = spec;
1296
+ return async (request, response) => {
1297
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
1298
+ const method = request.method ?? "GET";
1299
+ const send = (status, body2) => {
1300
+ response.writeHead(status, {
1301
+ "content-type": "application/json; charset=utf-8",
1302
+ "cache-control": "no-store"
1303
+ });
1304
+ response.end(JSON.stringify(body2));
1305
+ };
1306
+ const sendBytes = (status, contentType, body2) => {
1307
+ response.writeHead(status, {
1308
+ "content-type": contentType,
1309
+ "content-length": body2.byteLength,
1310
+ "cache-control": "no-store"
1311
+ });
1312
+ response.end(body2);
1313
+ };
1314
+ const sendData = async (load) => {
1315
+ try {
1316
+ send(200, await load());
1317
+ } catch (error) {
1318
+ send(502, { error: errorText(error) });
1319
+ }
1320
+ };
1321
+ let body;
1322
+ const context = {
1323
+ url,
1324
+ method,
1325
+ read: () => body ??= readJson(request).then(bodyOf),
1326
+ send,
1327
+ sendBytes,
1328
+ sendData
1329
+ };
1330
+ try {
1331
+ if (url.pathname === `${prefix}/status` && method === "GET") {
1332
+ send(200, await auth.getStatus());
1333
+ return;
1334
+ }
1335
+ if (url.pathname === `${prefix}/login` && method === "POST") {
1336
+ void auth.signIn();
1337
+ await auth.settleNotice(300);
1338
+ send(202, await auth.getStatus());
1339
+ return;
1340
+ }
1341
+ if (url.pathname === `${prefix}/logout` && method === "POST") {
1342
+ await auth.signOut();
1343
+ send(200, await auth.getStatus());
1344
+ return;
1345
+ }
1346
+ if (url.pathname === `${prefix}/sync` && method === "POST") {
1347
+ await auth.connect();
1348
+ send(200, await auth.getStatus());
1349
+ return;
1350
+ }
1351
+ for (const handler of spec.routes ?? []) {
1352
+ if (await handler(context)) return;
1353
+ }
1354
+ send(404, { error: "not-found", path: url.pathname });
1355
+ } catch (error) {
1356
+ send(error instanceof BadRequest ? 400 : 500, {
1357
+ error: errorText(error)
1358
+ });
1359
+ }
1360
+ };
1361
+ }
1362
+
1363
+ // ../integration-host/src/state.ts
1364
+ var RuntimeState = class {
1365
+ /** The last notice a running attempt published, so a card can show the URL. */
1366
+ notice;
1367
+ /** The last failure, cleared when a new attempt starts. */
1368
+ error;
1369
+ /** Settles when the in-flight attempt finishes. */
1370
+ running;
1371
+ /** The account facts this connection works with, once known. */
1372
+ facts;
1373
+ constructor() {
1374
+ this.notice = void 0;
1375
+ this.error = void 0;
1376
+ this.running = void 0;
1377
+ this.facts = void 0;
1378
+ }
1379
+ };
1380
+
1381
+ // src/host/store/site.ts
1382
+ var SITE_TOOL = "getAccessibleAtlassianResources";
1383
+ var SiteFacts = class _SiteFacts {
1384
+ siteUrl;
1385
+ cloudId;
1386
+ constructor(params) {
1387
+ this.siteUrl = params.siteUrl;
1388
+ this.cloudId = params.cloudId;
1389
+ }
1390
+ /** What the stored grant already knows, if anything. */
1391
+ static from(grant) {
1392
+ const extra = grant?.extra;
1393
+ return new _SiteFacts({
1394
+ ...typeof extra?.siteUrl === "string" ? { siteUrl: extra.siteUrl } : {},
1395
+ ...typeof extra?.cloudId === "string" ? { cloudId: extra.cloudId } : {}
1396
+ });
1397
+ }
1398
+ /**
1399
+ * Read the first site of an account's own list.
1400
+ * @param value - the result of `getAccessibleAtlassianResources`.
1401
+ * @returns the facts it names, or empty facts when it names none.
1402
+ */
1403
+ static parse(value) {
1404
+ for (const text of mcpText(value)) {
1405
+ const sites = parseJson(text);
1406
+ const record = asRecord(Array.isArray(sites) ? sites[0] : void 0);
1407
+ if (record === void 0) continue;
1408
+ const url = pickString(record, "url");
1409
+ const id = pickString(record, "id");
1410
+ if (url === void 0 && id === void 0) continue;
1411
+ return new _SiteFacts({
1412
+ ...url === void 0 ? {} : { siteUrl: url.replace(/\/+$/u, "") },
1413
+ ...id === void 0 ? {} : { cloudId: id }
1414
+ });
1415
+ }
1416
+ return new _SiteFacts({});
1417
+ }
1418
+ /** Whether neither the site nor the cloud id is known. */
1419
+ get isEmpty() {
1420
+ return this.siteUrl === void 0 && this.cloudId === void 0;
1421
+ }
1422
+ /** Whether both the site and the cloud id are known, so nothing is left to learn. */
1423
+ get isComplete() {
1424
+ return this.siteUrl !== void 0 && this.cloudId !== void 0;
1425
+ }
1426
+ };
1427
+ var siteFacts = {
1428
+ toolName: SITE_TOOL,
1429
+ from: (grant) => SiteFacts.from(grant),
1430
+ parse: (value) => SiteFacts.parse(value),
1431
+ merge: (known, learned) => new SiteFacts({
1432
+ siteUrl: learned.siteUrl ?? known?.siteUrl,
1433
+ cloudId: learned.cloudId ?? known?.cloudId
1434
+ }),
1435
+ remember: (facts) => ({
1436
+ ...facts.siteUrl === void 0 ? {} : { siteUrl: facts.siteUrl },
1437
+ ...facts.cloudId === void 0 ? {} : { cloudId: facts.cloudId }
1438
+ })
1439
+ };
1440
+ function parseJson(text) {
1441
+ try {
1442
+ return JSON.parse(text);
1443
+ } catch {
1444
+ return void 0;
1445
+ }
1446
+ }
1447
+
1448
+ // src/host/auth.ts
1449
+ var atlassianAuthSpec = {
1450
+ label: "Atlassian",
1451
+ logTag: "[atlassian]",
1452
+ facts: siteFacts,
1453
+ status: (base, facts) => ({
1454
+ ...base,
1455
+ siteUrl: facts?.siteUrl ?? null
1456
+ })
1457
+ };
1458
+
1459
+ // src/host/config.ts
1460
+ var DEFAULT_ENDPOINT = "https://mcp.atlassian.com/v1/mcp";
1461
+ var DEFAULT_REDIRECT_PORT = 8143;
1462
+ var PLUGIN_NAME = "dsh-atlassian";
1463
+ var DESIRED_SCOPES = [
1464
+ "offline_access",
1465
+ "read:jira-work",
1466
+ "write:jira-work",
1467
+ "read:jira-user",
1468
+ "read:page:confluence",
1469
+ "write:page:confluence",
1470
+ "search:confluence",
1471
+ "read:space:confluence",
1472
+ "read:jira:agent-interface",
1473
+ "write:jira:agent-interface",
1474
+ "search:jira:agent-interface",
1475
+ "read:confluence:agent-interface",
1476
+ "write:confluence:agent-interface",
1477
+ "search:confluence:agent-interface"
1478
+ ];
1479
+ var FALLBACK_SCOPES = [
1480
+ "offline_access",
1481
+ "read:jira-work",
1482
+ "write:jira-work",
1483
+ "read:jira-user",
1484
+ "read:page:confluence",
1485
+ "write:page:confluence",
1486
+ "search:confluence",
1487
+ "read:space:confluence"
1488
+ ];
1489
+ var resolveConfig = (input) => {
1490
+ return resolveEndpointConfig(input, {
1491
+ endpoint: DEFAULT_ENDPOINT,
1492
+ redirectPort: DEFAULT_REDIRECT_PORT
1493
+ });
1494
+ };
1495
+ var atlassianProfile = (config) => {
1496
+ return {
1497
+ scope: PLUGIN_NAME,
1498
+ label: "Atlassian",
1499
+ instance: DEFAULT_INSTANCE,
1500
+ endpoint: config.endpoint,
1501
+ toolNamespace: "atlassian",
1502
+ debug: config.debug,
1503
+ oauth: {
1504
+ redirectPort: config.redirectPort,
1505
+ scopes: DESIRED_SCOPES,
1506
+ fallbackScopes: FALLBACK_SCOPES,
1507
+ clientName: "DeepSeek Harness"
1508
+ }
1509
+ };
1510
+ };
1511
+
1512
+ // src/host/presentation/extract.ts
1513
+ var MINUTES_PER_DAY = 480;
1514
+ function formatDuration(seconds) {
1515
+ if (!Number.isFinite(seconds) || seconds <= 0) return void 0;
1516
+ const total = Math.round(seconds / 60);
1517
+ if (total < 1) return "<1m";
1518
+ const days = Math.floor(total / MINUTES_PER_DAY);
1519
+ const hours = Math.floor((total - days * MINUTES_PER_DAY) / 60);
1520
+ const minutes = total - days * MINUTES_PER_DAY - hours * 60;
1521
+ const parts = [
1522
+ days === 0 ? "" : `${days}d`,
1523
+ hours === 0 ? "" : `${hours}h`,
1524
+ minutes === 0 || days > 0 ? "" : `${minutes}m`
1525
+ ];
1526
+ return parts.filter(Boolean).join(" ");
1527
+ }
1528
+ var Extractor = class {
1529
+ /** Jira issue keys: an uppercase project key, a dash, and a number. */
1530
+ ISSUE_KEY = /\b[A-Z][A-Z0-9_]{0,9}-\d+\b/u;
1531
+ /** Bound the work a pathological payload can cause. */
1532
+ MAX_OBJECTS = 3e3;
1533
+ MAX_STRINGS = 800;
1534
+ MAX_DEPTH = 7;
1535
+ MAX_CARDS = 25;
1536
+ MAX_TEXT = 4e3;
1537
+ /** Longest description or body kept for a summary pass or a panel. */
1538
+ MAX_BODY = 8e3;
1539
+ /** Node types that end a line, so a flattened document keeps its shape. */
1540
+ BLOCK_TYPES = /* @__PURE__ */ new Set([
1541
+ "paragraph",
1542
+ "heading",
1543
+ "blockquote",
1544
+ "codeBlock",
1545
+ "listItem",
1546
+ "tableRow",
1547
+ "panel",
1548
+ "rule"
1549
+ ]);
1550
+ /**
1551
+ * Extract card data from one tool result.
1552
+ * @param options - the tool name, its arguments, and the returned value.
1553
+ * @returns the meta the client renders; `text` is always populated.
1554
+ */
1555
+ extract = (options) => {
1556
+ const texts = mcpText(options.value);
1557
+ const joined = texts.join("\n").trim();
1558
+ const roots = [];
1559
+ const structured = this.asRecord(options.value)?.structuredContent;
1560
+ if (structured !== void 0) roots.push(structured);
1561
+ roots.push(...mcpJson(options.value));
1562
+ const objects = [];
1563
+ const strings = [];
1564
+ for (const root of roots) {
1565
+ this.collectObjects(root, objects, 0);
1566
+ this.collectStrings(root, strings, 0);
1567
+ }
1568
+ this.collectStrings(joined, strings, 0);
1569
+ const issues = this.issueCards(
1570
+ objects,
1571
+ strings,
1572
+ options.siteUrl,
1573
+ options.bodies === true
1574
+ );
1575
+ const pages = issues.length > 0 ? [] : this.pageCards(
1576
+ objects,
1577
+ strings,
1578
+ options.siteUrl,
1579
+ options.bodies === true
1580
+ );
1581
+ const query = this.pick(
1582
+ options.args,
1583
+ "jql",
1584
+ "cql",
1585
+ "query",
1586
+ "searchString",
1587
+ "issueIdOrKey",
1588
+ "pageId",
1589
+ "cloudId"
1590
+ );
1591
+ const kind = issues.length > 0 ? "jira-issues" : pages.length > 0 ? "confluence-pages" : "text";
1592
+ const total = kind === "text" ? void 0 : this.totalOf(roots);
1593
+ return {
1594
+ kind,
1595
+ tool: options.tool,
1596
+ ...query === void 0 ? {} : { query },
1597
+ ...total === void 0 ? {} : { total },
1598
+ issues,
1599
+ pages,
1600
+ text: joined.slice(0, this.MAX_TEXT)
1601
+ };
1602
+ };
1603
+ /**
1604
+ * Flatten one rich field to the plain text a caller can use.
1605
+ *
1606
+ * The side panel reads a description or a comment body through this: the same
1607
+ * markdown-or-ADF handling the cards use, with the same bound.
1608
+ * @param value - a markdown string, an ADF tree, or anything else.
1609
+ * @returns the text, or undefined when there is none.
1610
+ */
1611
+ text = (value) => this.plainText(value);
1612
+ collectStrings = (node, out, depth) => {
1613
+ if (depth > this.MAX_DEPTH || node === null || node === void 0 || out.length >= this.MAX_STRINGS)
1614
+ return;
1615
+ if (typeof node === "string") {
1616
+ out.push(node);
1617
+ return;
1618
+ }
1619
+ if (Array.isArray(node)) {
1620
+ for (const item of node) this.collectStrings(item, out, depth + 1);
1621
+ return;
1622
+ }
1623
+ if (typeof node === "object") {
1624
+ for (const item of Object.values(node))
1625
+ this.collectStrings(item, out, depth + 1);
1626
+ }
1627
+ };
1628
+ collectObjects = (node, out, depth) => {
1629
+ if (depth > this.MAX_DEPTH || node === null || node === void 0 || out.length >= this.MAX_OBJECTS)
1630
+ return;
1631
+ if (Array.isArray(node)) {
1632
+ for (const item of node) this.collectObjects(item, out, depth + 1);
1633
+ return;
1634
+ }
1635
+ if (typeof node !== "object") return;
1636
+ out.push(node);
1637
+ for (const item of Object.values(node))
1638
+ this.collectObjects(item, out, depth + 1);
1639
+ };
1640
+ /** The first non-empty string among the named fields. */
1641
+ pick = (source, ...names) => {
1642
+ if (source === null || source === void 0 || typeof source !== "object")
1643
+ return void 0;
1644
+ const record = source;
1645
+ for (const name2 of names) {
1646
+ const value = record[name2];
1647
+ if (typeof value === "string" && value.trim() !== "") return value.trim();
1648
+ if (typeof value === "number") return String(value);
1649
+ }
1650
+ return void 0;
1651
+ };
1652
+ /** The first finite number among the named fields. */
1653
+ numberOf = (source, ...names) => {
1654
+ if (source === null || source === void 0 || typeof source !== "object")
1655
+ return void 0;
1656
+ const record = source;
1657
+ for (const name2 of names) {
1658
+ const value = record[name2];
1659
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1660
+ if (typeof value === "string" && value.trim() !== "") {
1661
+ const parsed = Number(value);
1662
+ if (Number.isFinite(parsed)) return parsed;
1663
+ }
1664
+ }
1665
+ return void 0;
1666
+ };
1667
+ /**
1668
+ * How many items the server says matched, when a root record reports it.
1669
+ *
1670
+ * Only the roots are read, never the hundreds of nested objects: `total` is
1671
+ * a page-level fact, and a nested one belongs to something else.
1672
+ */
1673
+ totalOf = (roots) => {
1674
+ let total;
1675
+ const raise = (candidate) => {
1676
+ if (candidate === void 0) return;
1677
+ total = total === void 0 ? candidate : Math.max(total, candidate);
1678
+ };
1679
+ for (const root of roots) {
1680
+ if (Array.isArray(root)) {
1681
+ raise(root.length);
1682
+ continue;
1683
+ }
1684
+ raise(
1685
+ this.numberOf(this.asRecord(root), "total", "totalCount", "totalSize")
1686
+ );
1687
+ }
1688
+ return total;
1689
+ };
1690
+ /** A duration estimate, in Jira's own words when it gave them. */
1691
+ estimateOf = (fields) => {
1692
+ const tracking = this.asRecord(fields.timetracking) ?? this.asRecord(fields.timeTracking);
1693
+ const stated = this.pick(tracking, "originalEstimate", "remainingEstimate");
1694
+ if (stated !== void 0) return stated;
1695
+ const seconds = this.numberOf(
1696
+ tracking,
1697
+ "originalEstimateSeconds",
1698
+ "remainingEstimateSeconds"
1699
+ );
1700
+ if (seconds !== void 0) return formatDuration(seconds);
1701
+ return formatDuration(
1702
+ this.numberOf(
1703
+ fields,
1704
+ "timeoriginalestimate",
1705
+ "aggregateoriginalestimate",
1706
+ "timeestimate",
1707
+ "aggregatetimeestimate"
1708
+ ) ?? 0
1709
+ );
1710
+ };
1711
+ /**
1712
+ * The plain text of a rich field.
1713
+ *
1714
+ * A description arrives as markdown text from one server generation and as
1715
+ * an Atlassian Document Format tree from the other, so this reads both: text
1716
+ * nodes in order, with a break between the blocks that carry one.
1717
+ */
1718
+ plainText = (value) => {
1719
+ if (typeof value === "string") return this.tidy(stripHtmlComments(value));
1720
+ const text = this.tidy(this.richText(value, 0));
1721
+ return text === "" ? void 0 : text;
1722
+ };
1723
+ /**
1724
+ * The plain text of a search snippet.
1725
+ *
1726
+ * A Confluence search result carries an `excerpt` that is not a document but
1727
+ * a fragment of rendered HTML, entities and all.
1728
+ */
1729
+ snippetText = (value) => {
1730
+ if (typeof value !== "string") return this.plainText(value);
1731
+ const stripped = value.replace(/<[^>]*>/gu, " ").replace(/&lt;/gu, "<").replace(/&gt;/gu, ">").replace(/&quot;/gu, '"').replace(/&#39;/gu, "'").replace(/&nbsp;/gu, " ").replace(/&amp;/gu, "&");
1732
+ return this.plainText(stripped);
1733
+ };
1734
+ richText = (node, depth) => {
1735
+ if (depth > this.MAX_DEPTH || node === null || node === void 0)
1736
+ return "";
1737
+ if (typeof node === "string") return node;
1738
+ if (typeof node === "number" || typeof node === "boolean")
1739
+ return ` ${String(node)} `;
1740
+ if (Array.isArray(node))
1741
+ return node.map((item) => this.richText(item, depth + 1)).join("");
1742
+ if (typeof node !== "object") return "";
1743
+ const record = node;
1744
+ const body = typeof record.text === "string" ? record.text : record.content === void 0 ? "" : this.richText(record.content, depth + 1);
1745
+ return this.BLOCK_TYPES.has(String(record.type)) ? `${body}
1746
+
1747
+ ` : body;
1748
+ };
1749
+ /** Collapse the whitespace a markdown or ADF body is full of. */
1750
+ 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);
1752
+ };
1753
+ asRecord = (value) => {
1754
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1755
+ };
1756
+ compact = (value) => {
1757
+ return Object.fromEntries(
1758
+ Object.entries(value).filter(
1759
+ ([, item]) => item !== void 0 && item !== ""
1760
+ )
1761
+ );
1762
+ };
1763
+ /** The first absolute `/browse/<key>` URL in the payload, else the site's own. */
1764
+ issueUrl = (strings, key, siteUrl) => {
1765
+ for (const text of strings) {
1766
+ if (!text.includes("/browse/") || !text.includes(key)) continue;
1767
+ const match = text.match(
1768
+ /https?:\/\/[^\s"')<>]*\/browse\/[A-Z][A-Z0-9_]{0,9}-\d+/u
1769
+ );
1770
+ if (match !== null) return match[0];
1771
+ }
1772
+ return siteUrl === void 0 ? void 0 : `${siteUrl}/browse/${key}`;
1773
+ };
1774
+ /**
1775
+ * Resolve a possibly relative Atlassian path against the site's own root.
1776
+ *
1777
+ * A Confluence search answers with `"/spaces/KEY/pages/123/Title"`, which is
1778
+ * relative to the wiki (`https://site.atlassian.net/wiki`), while a `webui`
1779
+ * link that already starts with `/wiki` is relative to the site itself. The
1780
+ * payload's own absolute URLs say which site that is; the learned site is
1781
+ * the fallback when the payload never spells one out.
1782
+ */
1783
+ absoluteUrl = (raw, strings, siteUrl) => {
1784
+ if (raw === void 0) return void 0;
1785
+ if (raw.startsWith("http")) return raw;
1786
+ if (!raw.startsWith("/")) return void 0;
1787
+ const site = this.siteBase(strings) ?? siteUrl;
1788
+ if (site === void 0) return void 0;
1789
+ return `${raw.startsWith("/wiki/") ? site : `${site}/wiki`}${raw}`;
1790
+ };
1791
+ /** The first `https://<site>.atlassian.net` the payload spells out. */
1792
+ siteBase = (strings) => {
1793
+ for (const text of strings) {
1794
+ const match = text.match(/https?:\/\/[^\s"')<>]*atlassian\.net/u);
1795
+ if (match !== null) return match[0];
1796
+ }
1797
+ return void 0;
1798
+ };
1799
+ issueCards = (objects, strings, siteUrl, bodies) => {
1800
+ const issues = /* @__PURE__ */ new Map();
1801
+ for (const node of objects) {
1802
+ const key = this.pick(node, "key");
1803
+ if (key === void 0 || !this.ISSUE_KEY.test(key) || issues.has(key))
1804
+ continue;
1805
+ const fields = this.asRecord(node.fields) ?? node;
1806
+ const rawText = bodies ? this.plainText(fields.description) : void 0;
1807
+ issues.set(
1808
+ key,
1809
+ this.compact({
1810
+ key,
1811
+ summary: this.pick(fields, "summary", "name", "title"),
1812
+ status: this.pick(this.asRecord(fields.status) ?? fields.status, "name") ?? this.pick(fields, "status"),
1813
+ assignee: this.pick(
1814
+ this.asRecord(fields.assignee) ?? fields.assignee,
1815
+ "displayName",
1816
+ "name"
1817
+ ) ?? this.pick(fields, "assignee"),
1818
+ priority: this.pick(
1819
+ this.asRecord(fields.priority) ?? fields.priority,
1820
+ "name"
1821
+ ) ?? this.pick(fields, "priority"),
1822
+ type: this.pick(
1823
+ this.asRecord(fields.issuetype) ?? fields.issuetype,
1824
+ "name"
1825
+ ) ?? this.pick(fields, "issuetype"),
1826
+ estimate: this.estimateOf(fields),
1827
+ url: this.issueUrl(strings, key, siteUrl),
1828
+ rawText
1829
+ })
1830
+ );
1831
+ if (issues.size >= this.MAX_CARDS) break;
1832
+ }
1833
+ return [...issues.values()];
1834
+ };
1835
+ pageCards = (objects, strings, siteUrl, bodies) => {
1836
+ const pages = /* @__PURE__ */ new Map();
1837
+ for (const node of objects) {
1838
+ const title = this.pick(node, "title");
1839
+ if (title === void 0) continue;
1840
+ const id = this.pick(node, "id");
1841
+ const raw = this.pick(node, "url") ?? this.pick(this.asRecord(node._links), "webui");
1842
+ if (id === void 0 && raw === void 0) continue;
1843
+ const identity = raw ?? id ?? title;
1844
+ if (pages.has(identity)) continue;
1845
+ const url = this.absoluteUrl(raw, strings, siteUrl) ?? (id !== void 0 && siteUrl !== void 0 ? `${siteUrl}/wiki/pages/viewpage.action?pageId=${id}` : void 0);
1846
+ const rawText = !bodies ? void 0 : node.body === void 0 ? this.snippetText(node.excerpt) : this.plainText(node.body);
1847
+ pages.set(
1848
+ identity,
1849
+ this.compact({
1850
+ id,
1851
+ title,
1852
+ url,
1853
+ space: this.pick(
1854
+ this.asRecord(node.space) ?? node.space,
1855
+ "name",
1856
+ "key"
1857
+ ),
1858
+ rawText
1859
+ })
1860
+ );
1861
+ if (pages.size >= this.MAX_CARDS) break;
1862
+ }
1863
+ return [...pages.values()];
1864
+ };
1865
+ };
1866
+ var extractor = new Extractor();
1867
+
1868
+ // src/host/correlation.ts
1869
+ var ISSUE_TOOL = "getJiraIssue";
1870
+ var ISSUE_FIELDS = ["summary", "status", "assignee", "issuetype", "priority"];
1871
+ var nameOf = (value) => typeof value === "string" && value !== "" ? value : pickString(asRecord(value) ?? {}, "displayName", "name", "value");
1872
+ var factOf = (key, value, siteUrl) => {
1873
+ const record = asRecord(value) ?? {};
1874
+ const fields = asRecord(record.fields) ?? record;
1875
+ const identity = pickString(record, "key") ?? key;
1876
+ const summary = extractor.text(pickString(fields, "summary"));
1877
+ const parts = [
1878
+ nameOf(fields.status),
1879
+ nameOf(fields.issuetype),
1880
+ nameOf(fields.priority),
1881
+ nameOf(fields.assignee)
1882
+ ].filter((part) => part !== void 0 && part !== "");
1883
+ return {
1884
+ id: `jira:${identity}`,
1885
+ provider: "Atlassian",
1886
+ kind: "task",
1887
+ label: "Jira issue",
1888
+ title: summary === void 0 ? identity : `${identity} \xB7 ${summary}`,
1889
+ ...siteUrl === void 0 ? {} : { url: `${siteUrl}/browse/${identity}` },
1890
+ ...parts.length === 0 ? {} : { meta: parts.join(" \xB7 ") }
1891
+ };
1892
+ };
1893
+ var atlassianCorrelationSource = (deps) => ({
1894
+ provider: "Atlassian",
1895
+ entry: "task",
1896
+ resolves: ["task"],
1897
+ resolve: async (ref) => {
1898
+ if (ref.kind !== "task") return [];
1899
+ const cloudId = deps.state.facts?.cloudId;
1900
+ if (cloudId === void 0)
1901
+ throw new Error(
1902
+ "no Atlassian site known yet; sign in again or run /atlassian sync"
1903
+ );
1904
+ const value = await deps.bridge.callTool(ISSUE_TOOL, {
1905
+ cloudId,
1906
+ issueIdOrKey: ref.key,
1907
+ fields: ISSUE_FIELDS,
1908
+ responseContentFormat: "markdown"
1909
+ });
1910
+ return [factOf(ref.key, value, deps.state.facts?.siteUrl)];
1911
+ }
1912
+ });
1913
+ var registerCorrelationSource = (ctx, deps) => {
1914
+ ctx.inject([CORRELATION_SOURCES], (scoped) => {
1915
+ scoped.effect(
1916
+ () => scoped.correlationSources.register(atlassianCorrelationSource(deps)),
1917
+ "atlassian: correlation source"
1918
+ );
1919
+ });
1920
+ };
1921
+
1922
+ // src/host/digest.ts
1923
+ var SEARCH_TOOL = "searchJiraIssuesUsingJql";
1924
+ var ISSUE_FIELDS2 = [
1925
+ "summary",
1926
+ "status",
1927
+ "assignee",
1928
+ "priority",
1929
+ "issuetype",
1930
+ "duedate"
1931
+ ];
1932
+ var JQL = {
1933
+ "jira-active": 'assignee = currentUser() AND statusCategory != Done AND (statusCategory = "In Progress" OR duedate <= now()) ORDER BY duedate ASC',
1934
+ "jira-queued": 'assignee = currentUser() AND statusCategory = "To Do" ORDER BY created DESC'
1935
+ };
1936
+ var toItem = (issue) => {
1937
+ const meta = [issue.status, issue.priority].filter(Boolean).join(" \xB7 ");
1938
+ return {
1939
+ id: issue.key,
1940
+ title: [issue.key, issue.summary].filter(Boolean).join(" \xB7 "),
1941
+ ...issue.url === void 0 ? {} : { url: issue.url },
1942
+ ...meta === "" ? {} : { meta }
1943
+ };
1944
+ };
1945
+ var atlassianDigestSource = (deps) => ({
1946
+ provider: "Atlassian",
1947
+ blocks: [
1948
+ {
1949
+ id: "jira-active",
1950
+ title: "Jira \xB7 In progress and overdue",
1951
+ defaultOn: true
1952
+ },
1953
+ {
1954
+ id: "jira-queued",
1955
+ title: "Jira \xB7 Assigned, not started",
1956
+ defaultOn: true
1957
+ }
1958
+ ],
1959
+ load: async (blockId, options) => {
1960
+ const jql = JQL[blockId];
1961
+ if (jql === void 0) throw new Error(`no Jira block named "${blockId}"`);
1962
+ const cloudId = deps.state.facts?.cloudId;
1963
+ if (cloudId === void 0)
1964
+ throw new Error(
1965
+ "the Atlassian site is not known yet; run /atlassian sync"
1966
+ );
1967
+ const value = await deps.bridge.callTool(SEARCH_TOOL, {
1968
+ cloudId,
1969
+ jql,
1970
+ fields: ISSUE_FIELDS2,
1971
+ maxResults: options.limit
1972
+ });
1973
+ return extractor.extract({
1974
+ tool: SEARCH_TOOL,
1975
+ args: {},
1976
+ value,
1977
+ siteUrl: deps.state.facts?.siteUrl
1978
+ }).issues.slice(0, options.limit).map(toItem);
1979
+ }
1980
+ });
1981
+ var registerDigestSource = (ctx, deps) => {
1982
+ ctx.inject([DIGEST_SOURCES], (scoped) => {
1983
+ scoped.effect(
1984
+ () => scoped.digestSources.register(atlassianDigestSource(deps)),
1985
+ "atlassian: digest source"
1986
+ );
1987
+ });
1988
+ };
1989
+
1990
+ // src/host/panels/panel.ts
1991
+ var COMMENT_LIMIT = 25;
1992
+ var PEOPLE_LIMIT = 8;
1993
+ var PanelApi = class {
1994
+ constructor(deps) {
1995
+ this.deps = deps;
1996
+ }
1997
+ /** The issue fields the panel renders. */
1998
+ ISSUE_FIELDS = [
1999
+ "summary",
2000
+ "description",
2001
+ "status",
2002
+ "issuetype",
2003
+ "priority",
2004
+ "assignee",
2005
+ "reporter",
2006
+ "created",
2007
+ "updated",
2008
+ "duedate",
2009
+ "labels",
2010
+ "resolution",
2011
+ "timetracking",
2012
+ "timeoriginalestimate"
2013
+ ];
2014
+ /** Everything one issue panel shows. */
2015
+ issue = async (key) => {
2016
+ const cloudId = this.cloudId();
2017
+ const [detail, transitions] = await Promise.all([
2018
+ this.deps.bridge.callTool("getJiraIssue", {
2019
+ cloudId,
2020
+ issueIdOrKey: key,
2021
+ fields: [...this.ISSUE_FIELDS, "comment"],
2022
+ responseContentFormat: "markdown"
2023
+ }),
2024
+ this.deps.bridge.callTool("getTransitionsForJiraIssue", {
2025
+ cloudId,
2026
+ issueIdOrKey: key
2027
+ }).then((value) => this.transitionsOf(value)).catch(() => [])
2028
+ ]);
2029
+ const record = this.issueRecord(detail);
2030
+ if (record === void 0)
2031
+ throw new Error(`Atlassian returned no issue for ${key}`);
2032
+ const fields = asRecord(record.fields) ?? record;
2033
+ const identity = pickString(record, "key") ?? key;
2034
+ const summary = extractor.text(pickString(fields, "summary"));
2035
+ return {
2036
+ kind: "issue",
2037
+ key: identity,
2038
+ ...summary === void 0 ? {} : { summary },
2039
+ type: pickString(asRecord(fields.issuetype) ?? fields.issuetype, "name"),
2040
+ status: pickString(asRecord(fields.status) ?? fields.status, "name"),
2041
+ priority: pickString(
2042
+ asRecord(fields.priority) ?? fields.priority,
2043
+ "name"
2044
+ ),
2045
+ assignee: this.personOf(fields.assignee),
2046
+ reporter: pickString(
2047
+ asRecord(fields.reporter) ?? fields.reporter,
2048
+ "displayName",
2049
+ "name"
2050
+ ),
2051
+ estimate: this.estimateOf(fields),
2052
+ due: pickString(fields, "duedate"),
2053
+ labels: Array.isArray(fields.labels) ? fields.labels.filter(
2054
+ (label) => typeof label === "string"
2055
+ ) : [],
2056
+ created: pickString(fields, "created"),
2057
+ updated: pickString(fields, "updated"),
2058
+ description: extractor.text(fields.description),
2059
+ url: this.deps.state.facts?.siteUrl === void 0 ? void 0 : `${this.deps.state.facts?.siteUrl}/browse/${identity}`,
2060
+ comments: this.commentsOf(fields.comment),
2061
+ transitions
2062
+ };
2063
+ };
2064
+ /** Everything one page panel shows. */
2065
+ page = async (id) => {
2066
+ const cloudId = this.cloudId();
2067
+ const [detail, comments] = await Promise.all([
2068
+ this.deps.bridge.callTool("getConfluencePage", {
2069
+ cloudId,
2070
+ pageId: id,
2071
+ contentFormat: "markdown"
2072
+ }),
2073
+ this.deps.bridge.callTool("getConfluencePageFooterComments", {
2074
+ cloudId,
2075
+ pageId: id,
2076
+ limit: COMMENT_LIMIT
2077
+ }).catch(() => void 0)
2078
+ ]);
2079
+ const record = this.firstRecord(detail);
2080
+ if (record === void 0)
2081
+ throw new Error(`Atlassian returned no page for ${id}`);
2082
+ const title = pickString(record, "title") ?? id;
2083
+ const webui = pickString(asRecord(record._links), "webui");
2084
+ return {
2085
+ kind: "page",
2086
+ id: pickString(record, "id") ?? id,
2087
+ title,
2088
+ space: pickString(asRecord(record.space) ?? record.space, "name", "key"),
2089
+ 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
+ body: extractor.text(record.body),
2091
+ comments: this.commentsOf(asRecord(comments)?.results)
2092
+ };
2093
+ };
2094
+ /** People the site matches to one search string. */
2095
+ people = async (query) => {
2096
+ const value = await this.deps.bridge.callTool("lookupJiraAccountId", {
2097
+ cloudId: this.cloudId(),
2098
+ searchString: query,
2099
+ showAvatar: false
2100
+ });
2101
+ const users = this.userList(value);
2102
+ return users.map((user) => ({
2103
+ name: pickString(user, "displayName") ?? "",
2104
+ accountId: pickString(user, "accountId") ?? ""
2105
+ })).filter((person) => person.name !== "" && person.accountId !== "").slice(0, PEOPLE_LIMIT);
2106
+ };
2107
+ /**
2108
+ * The signed-in user, as the site names them.
2109
+ *
2110
+ * `atlassianUserInfo` answers about the account the connection belongs to, in
2111
+ * a shape of its own; the first record that carries an account id is the
2112
+ * answer. A site that names none leaves the panel without a self-assignment
2113
+ * rather than offering one Jira would refuse.
2114
+ * @returns the reader, or undefined when the answer names no account.
2115
+ */
2116
+ me = async () => {
2117
+ const value = await this.deps.bridge.callTool("atlassianUserInfo", {});
2118
+ return this.personIn(value);
2119
+ };
2120
+ /** Move one issue through a workflow transition. */
2121
+ transition = async (key, transitionId) => {
2122
+ await this.deps.bridge.callTool("transitionJiraIssue", {
2123
+ cloudId: this.cloudId(),
2124
+ issueIdOrKey: key,
2125
+ transition: { id: transitionId }
2126
+ });
2127
+ };
2128
+ /** Set or clear one issue's assignee. */
2129
+ assign = async (key, accountId) => {
2130
+ await this.deps.bridge.callTool("editJiraIssue", {
2131
+ cloudId: this.cloudId(),
2132
+ issueIdOrKey: key,
2133
+ fields: { assignee: accountId === null ? null : { accountId } }
2134
+ });
2135
+ };
2136
+ /** The connected site's cloud id, or a message the user can act on. */
2137
+ cloudId = () => {
2138
+ if (this.deps.state.facts?.cloudId === void 0)
2139
+ throw new Error(
2140
+ "no Atlassian site known yet \u2014 sign in from Settings, then try again"
2141
+ );
2142
+ return this.deps.state.facts?.cloudId;
2143
+ };
2144
+ /** Jira's own estimate words, or a duration rendered from raw seconds. */
2145
+ estimateOf = (fields) => {
2146
+ const tracking = asRecord(fields.timetracking);
2147
+ const stated = pickString(
2148
+ tracking,
2149
+ "originalEstimate",
2150
+ "remainingEstimate"
2151
+ );
2152
+ if (stated !== void 0) return stated;
2153
+ const seconds = pickNumber(
2154
+ tracking,
2155
+ "originalEstimateSeconds",
2156
+ "remainingEstimateSeconds"
2157
+ );
2158
+ if (seconds !== void 0) return formatDuration(seconds);
2159
+ return formatDuration(
2160
+ pickNumber(fields, "timeoriginalestimate", "timeestimate") ?? 0
2161
+ );
2162
+ };
2163
+ personOf = (value) => {
2164
+ const record = asRecord(value);
2165
+ const accountId = pickString(record, "accountId");
2166
+ const name2 = pickString(record, "displayName", "name");
2167
+ if (accountId === void 0 || name2 === void 0) return void 0;
2168
+ return { accountId, name: name2 };
2169
+ };
2170
+ /** The Jira issue inside one `getJiraIssue` result. */
2171
+ issueRecord = (value) => this.firstRecord(value, (record) => asRecord(record.fields) !== void 0);
2172
+ /** The first JSON record in a result, or the first one a predicate accepts. */
2173
+ firstRecord = (value, accept = () => true) => {
2174
+ for (const root of mcpJson(value)) {
2175
+ const record = asRecord(root);
2176
+ if (record !== void 0 && accept(record)) return record;
2177
+ if (!Array.isArray(root)) continue;
2178
+ for (const item of root) {
2179
+ const candidate = asRecord(item);
2180
+ if (candidate !== void 0 && accept(candidate)) return candidate;
2181
+ }
2182
+ }
2183
+ return void 0;
2184
+ };
2185
+ /** The comment list one issue or page carries. */
2186
+ commentsOf = (value) => {
2187
+ const list = Array.isArray(value) ? value : Array.isArray(asRecord(value)?.comments) ? asRecord(value)?.comments : [];
2188
+ const comments = [];
2189
+ for (const item of list) {
2190
+ const record = asRecord(item);
2191
+ if (record === void 0) continue;
2192
+ const body = extractor.text(record.body);
2193
+ if (body === void 0) continue;
2194
+ const author = pickString(
2195
+ asRecord(record.author) ?? record.author,
2196
+ "displayName",
2197
+ "publicName",
2198
+ "name"
2199
+ );
2200
+ comments.push({
2201
+ body,
2202
+ ...author === void 0 ? {} : { author },
2203
+ ...pickString(record, "created") === void 0 ? {} : { created: pickString(record, "created") }
2204
+ });
2205
+ if (comments.length >= COMMENT_LIMIT) break;
2206
+ }
2207
+ return comments;
2208
+ };
2209
+ /** The transitions a workflow currently offers. */
2210
+ transitionsOf = (value) => {
2211
+ const record = this.firstRecord(value);
2212
+ const list = Array.isArray(record?.transitions) ? record.transitions : [];
2213
+ const transitions = [];
2214
+ for (const item of list) {
2215
+ const candidate = asRecord(item);
2216
+ if (candidate === void 0) continue;
2217
+ const id = pickString(candidate, "id");
2218
+ const name2 = pickString(candidate, "name");
2219
+ if (id === void 0 || name2 === void 0) continue;
2220
+ const to = asRecord(candidate.to);
2221
+ transitions.push({
2222
+ id,
2223
+ name: name2,
2224
+ ...pickString(to, "name") === void 0 ? {} : { to: pickString(to, "name") },
2225
+ // Jira omits `isAvailable` when the transition is offered.
2226
+ available: candidate.isAvailable !== false,
2227
+ needsFields: candidate.hasScreen === true
2228
+ });
2229
+ }
2230
+ return transitions;
2231
+ };
2232
+ /** The user list inside one `lookupJiraAccountId` result. */
2233
+ userList = (value) => {
2234
+ for (const root of mcpJson(value)) {
2235
+ const users = asRecord(asRecord(asRecord(root)?.data)?.users);
2236
+ const list = users?.users;
2237
+ if (Array.isArray(list))
2238
+ return list.map((item) => asRecord(item)).filter(
2239
+ (item) => item !== void 0
2240
+ );
2241
+ }
2242
+ return [];
2243
+ };
2244
+ /** The first person named anywhere in one tool result. */
2245
+ personIn = (value) => {
2246
+ for (const root of mcpJson(value)) {
2247
+ const person = this.personOf(root) ?? this.personOf(asRecord(asRecord(root)?.data));
2248
+ if (person !== void 0) return person;
2249
+ }
2250
+ return this.personOf(this.firstRecord(value));
2251
+ };
2252
+ /** A page's own path, resolved against the site's wiki. */
2253
+ wikiUrl = (raw, siteUrl) => {
2254
+ if (raw.startsWith("http")) return raw;
2255
+ if (!raw.startsWith("/") || siteUrl === void 0) return void 0;
2256
+ return `${raw.startsWith("/wiki/") ? siteUrl : `${siteUrl}/wiki`}${raw}`;
2257
+ };
2258
+ };
2259
+
2260
+ // src/host/surface/command.ts
2261
+ var COMMAND_NAME = "atlassian";
2262
+ var createCommandHandler = (auth) => {
2263
+ return createIntegrationCommand(
2264
+ { name: COMMAND_NAME, label: "Atlassian" },
2265
+ auth
2266
+ );
2267
+ };
2268
+
2269
+ // src/host/surface/http.ts
2270
+ var ROUTE_PREFIX = "/dsh-atlassian";
2271
+ var panelRoutes = (panel) => [
2272
+ async (route) => {
2273
+ if (route.url.pathname !== `${ROUTE_PREFIX}/issue` || route.method !== "GET")
2274
+ return false;
2275
+ const key = route.url.searchParams.get("key") ?? "";
2276
+ await route.sendData(() => panel.issue(key));
2277
+ return true;
2278
+ },
2279
+ async (route) => {
2280
+ if (route.url.pathname !== `${ROUTE_PREFIX}/page` || route.method !== "GET")
2281
+ return false;
2282
+ const id = route.url.searchParams.get("id") ?? "";
2283
+ await route.sendData(() => panel.page(id));
2284
+ return true;
2285
+ },
2286
+ async (route) => {
2287
+ if (route.url.pathname !== `${ROUTE_PREFIX}/people` || route.method !== "GET")
2288
+ return false;
2289
+ const query = route.url.searchParams.get("q") ?? "";
2290
+ await route.sendData(() => panel.people(query));
2291
+ return true;
2292
+ },
2293
+ async (route) => {
2294
+ if (route.url.pathname !== `${ROUTE_PREFIX}/me` || route.method !== "GET")
2295
+ return false;
2296
+ await route.sendData(async () => ({
2297
+ person: await panel.me() ?? null
2298
+ }));
2299
+ return true;
2300
+ },
2301
+ async (route) => {
2302
+ if (route.url.pathname !== `${ROUTE_PREFIX}/issue/transition` || route.method !== "POST")
2303
+ return false;
2304
+ const body = await route.read();
2305
+ const key = body.required("key");
2306
+ const transitionId = body.required("transitionId");
2307
+ await route.sendData(async () => {
2308
+ await panel.transition(key, transitionId);
2309
+ return await panel.issue(key);
2310
+ });
2311
+ return true;
2312
+ },
2313
+ async (route) => {
2314
+ if (route.url.pathname !== `${ROUTE_PREFIX}/issue/assignee` || route.method !== "POST")
2315
+ return false;
2316
+ const body = await route.read();
2317
+ const key = body.required("key");
2318
+ const accountId = body.fields?.accountId;
2319
+ if (accountId !== null && typeof accountId !== "string")
2320
+ throw new BadRequest('the request needs an "accountId" or null');
2321
+ await route.sendData(async () => {
2322
+ await panel.assign(key, accountId);
2323
+ return await panel.issue(key);
2324
+ });
2325
+ return true;
2326
+ }
2327
+ ];
2328
+ var createApiHandler = (deps) => {
2329
+ return createIntegrationRoute({
2330
+ prefix: ROUTE_PREFIX,
2331
+ auth: deps.auth,
2332
+ routes: panelRoutes(deps.panel)
2333
+ });
2334
+ };
2335
+
2336
+ // src/host/presentation/gist.ts
2337
+ var MAX_BODY_CHARS = 900;
2338
+ var MAX_PROMPT_CHARS = 12e3;
2339
+ var MAX_GIST_CHARS = 240;
2340
+ var TIMEOUT_MS = 2e4;
2341
+ var SYSTEM_PROMPT = [
2342
+ "You write the one-line summary a card shows for each numbered item below.",
2343
+ "Each item is a Jira issue or a Confluence page; its text may be in any language.",
2344
+ "Answer with exactly one line per item, formatted `<number>: <sentence>`.",
2345
+ "Say what the item actually asks for or contains, in the item's own language.",
2346
+ "At most 25 words. No markdown, no quotes, no bullet characters, no preamble, no closing remarks."
2347
+ ].join(" ");
2348
+ function modelRoute(exec) {
2349
+ const config = exec.agent?.session.requestHeader()?.config;
2350
+ const provider = config?.provider ?? exec.agent?.options.provider;
2351
+ const model = config?.model ?? exec.agent?.options.model;
2352
+ if (provider === void 0 || model === void 0) return void 0;
2353
+ if (provider === "" || model === "") return void 0;
2354
+ return { provider, model };
2355
+ }
2356
+ function parseGists(text, count) {
2357
+ const gists = new Array(count).fill(void 0);
2358
+ for (const line of text.split("\n")) {
2359
+ const match = line.trim().replace(/^[-*•]\s*/u, "").match(/^(\d{1,3})\s*[).:\-–—]\s*(.+)$/u);
2360
+ if (match === null) continue;
2361
+ const index = Number(match[1]) - 1;
2362
+ const sentence = match[2]?.replace(/^["'«“]+|["'»”]+$/gu, "").replace(/\s+/gu, " ").trim();
2363
+ if (index < 0 || index >= count) continue;
2364
+ if (gists[index] !== void 0) continue;
2365
+ if (sentence === void 0 || sentence === "") continue;
2366
+ gists[index] = sentence.slice(0, MAX_GIST_CHARS);
2367
+ }
2368
+ return gists;
2369
+ }
2370
+ async function summarizeGists(ctx, targets, options) {
2371
+ if (targets.length === 0) return void 0;
2372
+ const route = options.route;
2373
+ if (route === void 0) return void 0;
2374
+ const llm = ctx.get("llm");
2375
+ if (llm === void 0) return void 0;
2376
+ const prompt = buildPrompt(targets);
2377
+ if (prompt === "") return void 0;
2378
+ const signal = options.signal === void 0 ? AbortSignal.timeout(TIMEOUT_MS) : AbortSignal.any([options.signal, AbortSignal.timeout(TIMEOUT_MS)]);
2379
+ let answer = "";
2380
+ try {
2381
+ const stream = llm.stream({
2382
+ provider: route.provider,
2383
+ model: route.model,
2384
+ system: SYSTEM_PROMPT,
2385
+ messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
2386
+ temperature: 0,
2387
+ signal
2388
+ });
2389
+ for await (const chunk of stream) {
2390
+ if (chunk.type === "text-delta") answer += chunk.text;
2391
+ }
2392
+ } catch {
2393
+ return void 0;
2394
+ }
2395
+ const sentences = parseGists(answer, targets.length);
2396
+ const gists = [];
2397
+ targets.forEach((target, index) => {
2398
+ const sentence = sentences[index];
2399
+ if (sentence !== void 0) gists.push({ id: target.id, text: sentence });
2400
+ });
2401
+ return gists.length === 0 ? void 0 : gists;
2402
+ }
2403
+ function buildPrompt(targets) {
2404
+ const parts = [];
2405
+ let used = 0;
2406
+ targets.forEach((target, index) => {
2407
+ const heading = `${index + 1}. ${target.title}`;
2408
+ const room = MAX_PROMPT_CHARS - used - heading.length;
2409
+ const body = room <= 0 ? "" : target.body.slice(0, Math.min(room, MAX_BODY_CHARS));
2410
+ used += heading.length + body.length;
2411
+ parts.push(body === "" ? heading : `${heading}
2412
+ ${body}`);
2413
+ });
2414
+ return parts.join("\n\n");
2415
+ }
2416
+
2417
+ // src/host/tools/present.ts
2418
+ function stringList2(value) {
2419
+ if (!Array.isArray(value)) return [];
2420
+ return value.filter(
2421
+ (item) => typeof item === "string" && item !== ""
2422
+ );
2423
+ }
2424
+ function optionalString(value) {
2425
+ return typeof value === "string" && value !== "" ? value : void 0;
2426
+ }
2427
+ var PresentTool = class {
2428
+ /** The wire name the agent calls. */
2429
+ TOOL_NAME = "atlassian_present";
2430
+ /** Never put more than this many items on one card. */
2431
+ MAX_ITEMS = 25;
2432
+ /** Fields that make a card useful without making the call enormous. */
2433
+ ISSUE_FIELDS = [
2434
+ "summary",
2435
+ "status",
2436
+ "assignee",
2437
+ "priority",
2438
+ "issuetype",
2439
+ "timetracking",
2440
+ "timeoriginalestimate",
2441
+ // Read to write one sentence per issue, then dropped again: the recorded
2442
+ // card carries the sentence, never the description.
2443
+ "description"
2444
+ ];
2445
+ /**
2446
+ * Build the tool definition for one plugin instance.
2447
+ * @param ctx - host context; the model service is read from it for summaries.
2448
+ * @param state - runtime state holding the MCP connection and cloud id.
2449
+ */
2450
+ create = (deps) => ({
2451
+ name: this.TOOL_NAME,
2452
+ description: "Show Jira issues and/or Confluence pages to the user as a card under your answer. Use this whenever the user asks to see work \u2014 a daily plan, a shortlist, a single issue, or search results \u2014 instead of only describing it in prose. Resolve the content by issue keys, by page ids, by JQL, or by CQL. Each row carries a link to the item, its status, assignee, priority, type and estimate, plus one sentence summarizing its description, so the user can act without opening the issue. Call it once per card, then write your answer; do not repeat the card's contents as a list in the text.",
2453
+ parameters: {
2454
+ type: "object",
2455
+ additionalProperties: false,
2456
+ properties: {
2457
+ title: {
2458
+ type: "string",
2459
+ description: 'Optional heading for the card, such as "\u041F\u043B\u0430\u043D \u043D\u0430 \u0434\u0435\u043D\u044C". Omit for an untitled card.'
2460
+ },
2461
+ layout: {
2462
+ type: "string",
2463
+ enum: ["auto", "single", "compact", "list"],
2464
+ description: "auto (default) picks by item count: one item as a full card, two to four as compact rows, more as a dense list. A card longer than four rows scrolls and shows how many items matched. Force one when the content calls for it."
2465
+ },
2466
+ issues: {
2467
+ type: "array",
2468
+ items: { type: "string" },
2469
+ description: 'Issue keys to show, for example ["DEV-6477", "DQ-66"].'
2470
+ },
2471
+ pages: {
2472
+ type: "array",
2473
+ items: { type: "string" },
2474
+ description: "Confluence page ids to show."
2475
+ },
2476
+ jql: {
2477
+ type: "string",
2478
+ description: 'A JQL query whose results become the card, for example "assignee = currentUser() AND status != Done ORDER BY priority DESC".'
2479
+ },
2480
+ cql: {
2481
+ type: "string",
2482
+ description: "A CQL query whose Confluence results become the card."
2483
+ },
2484
+ cloudId: {
2485
+ type: "string",
2486
+ description: "Atlassian cloud id. Omit unless the user has several sites: the plugin remembers it."
2487
+ }
2488
+ }
2489
+ },
2490
+ output: {
2491
+ schema: {
2492
+ type: "object",
2493
+ additionalProperties: false,
2494
+ properties: {
2495
+ shown: { type: "integer" },
2496
+ summary: { type: "string" },
2497
+ failed: { type: "array", items: { type: "string" } },
2498
+ // The card payload, carried to the browser as `result.meta`.
2499
+ // Left open on purpose: it is presentation data, and the browser
2500
+ // half validates it before rendering.
2501
+ presentation: { type: "object", additionalProperties: true }
2502
+ },
2503
+ required: ["shown", "summary", "failed", "presentation"]
2504
+ },
2505
+ render: (_args, value) => [
2506
+ { type: "text", text: value.summary }
2507
+ ],
2508
+ // The browser half reads this back as `result.meta` and renders the
2509
+ // card. It never reaches the model: the text above is what the model
2510
+ // reads, and this is presentation only.
2511
+ presentationMeta: (_args, value) => value.presentation
2512
+ },
2513
+ execute: async (rawArgs, exec) => {
2514
+ const args = this.parseArgs(rawArgs);
2515
+ const outcome = await this.resolve(deps, args, exec);
2516
+ const viewed = this.describe(outcome, args);
2517
+ const summary = outcome.failed.length === 0 ? viewed : `${viewed}
2518
+ Could not resolve: ${outcome.failed.join(", ")}`;
2519
+ const presentation = {
2520
+ callId: String(exec.callId),
2521
+ ...outcome.title === void 0 ? {} : { title: outcome.title },
2522
+ layout: outcome.layout,
2523
+ ...outcome.total === void 0 ? {} : { total: outcome.total },
2524
+ items: outcome.items
2525
+ };
2526
+ return {
2527
+ shown: outcome.items.length,
2528
+ summary,
2529
+ failed: [...outcome.failed],
2530
+ presentation
2531
+ };
2532
+ }
2533
+ });
2534
+ /** Validate the model's arguments; unknown keys are ignored. */
2535
+ parseArgs(rawArgs) {
2536
+ const args = rawArgs !== null && typeof rawArgs === "object" ? rawArgs : {};
2537
+ const layout = optionalString(args.layout);
2538
+ return {
2539
+ title: optionalString(args.title),
2540
+ layout: layout === "auto" || layout === "single" || layout === "compact" || layout === "list" ? layout : void 0,
2541
+ cloudId: optionalString(args.cloudId),
2542
+ issues: stringList2(args.issues),
2543
+ pages: stringList2(args.pages),
2544
+ jql: optionalString(args.jql),
2545
+ cql: optionalString(args.cql)
2546
+ };
2547
+ }
2548
+ /** Resolve every requested source into card items. */
2549
+ resolve = async (deps, args, exec) => {
2550
+ const cloudId = args.cloudId ?? deps.state.facts?.cloudId;
2551
+ if (cloudId === void 0)
2552
+ throw new Error(
2553
+ "no Atlassian site known yet; run a Jira or Confluence search first, or sign in again"
2554
+ );
2555
+ const items = [];
2556
+ const failed = [];
2557
+ let matched = 0;
2558
+ const push = (candidate) => {
2559
+ for (const item of candidate) {
2560
+ if (items.length >= this.MAX_ITEMS) return;
2561
+ items.push(item);
2562
+ }
2563
+ };
2564
+ for (const key of args.issues) {
2565
+ try {
2566
+ const value = await deps.bridge.callTool("getJiraIssue", {
2567
+ cloudId,
2568
+ issueIdOrKey: key,
2569
+ fields: this.ISSUE_FIELDS
2570
+ });
2571
+ const found = this.issuesOf(value, deps.state.facts?.siteUrl, key);
2572
+ push(found.items);
2573
+ matched += found.total ?? found.items.length;
2574
+ } catch {
2575
+ failed.push(key);
2576
+ }
2577
+ }
2578
+ if (args.jql !== void 0) {
2579
+ const value = await deps.bridge.callTool("searchJiraIssuesUsingJql", {
2580
+ cloudId,
2581
+ jql: args.jql,
2582
+ fields: this.ISSUE_FIELDS,
2583
+ maxResults: this.MAX_ITEMS
2584
+ });
2585
+ const found = this.issuesOf(value, deps.state.facts?.siteUrl);
2586
+ push(found.items);
2587
+ matched += found.total ?? found.items.length;
2588
+ }
2589
+ for (const id of args.pages) {
2590
+ try {
2591
+ const value = await deps.bridge.callTool("getConfluencePage", {
2592
+ cloudId,
2593
+ pageId: id
2594
+ });
2595
+ const found = this.pagesOf(value, deps.state.facts?.siteUrl, id);
2596
+ push(found.items);
2597
+ matched += found.total ?? found.items.length;
2598
+ } catch {
2599
+ failed.push(id);
2600
+ }
2601
+ }
2602
+ if (args.cql !== void 0) {
2603
+ const value = await deps.bridge.callTool("searchConfluenceUsingCql", {
2604
+ cloudId,
2605
+ cql: args.cql,
2606
+ limit: this.MAX_ITEMS
2607
+ });
2608
+ const found = this.pagesOf(value, deps.state.facts?.siteUrl);
2609
+ push(found.items);
2610
+ matched += found.total ?? found.items.length;
2611
+ }
2612
+ if (items.length === 0 && failed.length === 0)
2613
+ throw new Error("nothing to show: pass issues, pages, jql, or cql");
2614
+ const gists = await this.summarize(deps.ctx, items, exec);
2615
+ const projected = items.map(
2616
+ (item, index) => item.kind === "issue" ? {
2617
+ kind: "issue",
2618
+ issue: this.issueWire(item.issue, gists.get(String(index)))
2619
+ } : {
2620
+ kind: "page",
2621
+ page: this.pageWire(item.page, gists.get(String(index)))
2622
+ }
2623
+ );
2624
+ return {
2625
+ title: args.title,
2626
+ layout: this.resolveLayout(args.layout, projected.length),
2627
+ items: projected,
2628
+ total: matched > projected.length ? matched : void 0,
2629
+ failed
2630
+ };
2631
+ };
2632
+ /**
2633
+ * Compress the long text behind each item into one sentence.
2634
+ *
2635
+ * A card is worth waiting a moment for, not a minute: the pass is bounded by
2636
+ * its own timeout and any failure simply leaves the items without summaries.
2637
+ */
2638
+ summarize = async (ctx, items, exec) => {
2639
+ const targets = items.flatMap((item, index) => {
2640
+ const title = item.kind === "issue" ? item.issue.summary ?? item.issue.key : item.page.title;
2641
+ const body = (item.kind === "issue" ? item.issue.rawText : item.page.rawText) ?? "";
2642
+ return body === "" ? [] : [{ id: String(index), title, body }];
2643
+ });
2644
+ if (targets.length === 0) return /* @__PURE__ */ new Map();
2645
+ const gists = await summarizeGists(
2646
+ ctx,
2647
+ targets,
2648
+ { route: modelRoute(exec), signal: exec.signal }
2649
+ );
2650
+ return new Map((gists ?? []).map((gist) => [gist.id, gist.text]));
2651
+ };
2652
+ /**
2653
+ * The recorded shape of one issue.
2654
+ *
2655
+ * Built field by field rather than spread: `rawText` is the description the
2656
+ * compression pass read, and the card must not carry a word of it.
2657
+ */
2658
+ issueWire = (issue, gist) => ({
2659
+ key: issue.key,
2660
+ ...issue.summary === void 0 ? {} : { summary: issue.summary },
2661
+ ...issue.status === void 0 ? {} : { status: issue.status },
2662
+ ...issue.assignee === void 0 ? {} : { assignee: issue.assignee },
2663
+ ...issue.priority === void 0 ? {} : { priority: issue.priority },
2664
+ ...issue.type === void 0 ? {} : { type: issue.type },
2665
+ ...issue.estimate === void 0 ? {} : { estimate: issue.estimate },
2666
+ ...gist === void 0 ? {} : { description: gist },
2667
+ ...issue.url === void 0 ? {} : { url: issue.url }
2668
+ });
2669
+ /** The recorded shape of one page; see {@link issueWire}. */
2670
+ pageWire = (page, gist) => ({
2671
+ title: page.title,
2672
+ ...page.id === void 0 ? {} : { id: page.id },
2673
+ ...page.url === void 0 ? {} : { url: page.url },
2674
+ ...page.space === void 0 ? {} : { space: page.space },
2675
+ ...gist === void 0 ? {} : { excerpt: gist }
2676
+ });
2677
+ /** Normalize one Jira result; `fallbackKey` keeps a single-issue fetch labelled. */
2678
+ issuesOf(value, siteUrl, fallbackKey) {
2679
+ const meta = extractor.extract({
2680
+ tool: "getJiraIssue",
2681
+ args: {},
2682
+ value,
2683
+ siteUrl,
2684
+ // The description is read here and compressed before it is recorded.
2685
+ bodies: true
2686
+ });
2687
+ const issues = meta.issues;
2688
+ if (issues.length > 0)
2689
+ return {
2690
+ items: issues.map((issue) => ({ kind: "issue", issue })),
2691
+ total: meta.total
2692
+ };
2693
+ return {
2694
+ items: fallbackKey === void 0 ? [] : [{ kind: "issue", issue: { key: fallbackKey } }],
2695
+ total: meta.total
2696
+ };
2697
+ }
2698
+ /** Normalize one Confluence result; `fallbackId` keeps a single fetch labelled. */
2699
+ pagesOf(value, siteUrl, fallbackId) {
2700
+ const meta = extractor.extract({
2701
+ tool: "getConfluencePage",
2702
+ args: {},
2703
+ value,
2704
+ siteUrl,
2705
+ bodies: true
2706
+ });
2707
+ const pages = meta.pages;
2708
+ if (pages.length > 0)
2709
+ return {
2710
+ items: pages.map((page) => ({ kind: "page", page })),
2711
+ total: meta.total
2712
+ };
2713
+ return {
2714
+ items: fallbackId === void 0 ? [] : [{ kind: "page", page: { id: fallbackId, title: fallbackId } }],
2715
+ total: meta.total
2716
+ };
2717
+ }
2718
+ /** Pick the layout the caller asked for, or infer one from the item count. */
2719
+ resolveLayout(input, count) {
2720
+ if (input === "single" || input === "compact" || input === "list")
2721
+ return input;
2722
+ if (count <= 1) return "single";
2723
+ if (count <= 4) return "compact";
2724
+ return "list";
2725
+ }
2726
+ /** The text the model reads: what the card shows, in order. */
2727
+ describe(outcome, args) {
2728
+ const issues = outcome.items.filter((item) => item.kind === "issue");
2729
+ const pages = outcome.items.filter((item) => item.kind === "page");
2730
+ const counts = [
2731
+ issues.length > 0 ? `${issues.length} issue${issues.length === 1 ? "" : "s"}` : "",
2732
+ pages.length > 0 ? `${pages.length} page${pages.length === 1 ? "" : "s"}` : ""
2733
+ ].filter(Boolean);
2734
+ const heading = outcome.title === void 0 ? "Shown as a card" : `Shown as a card titled "${outcome.title}"`;
2735
+ const lines = [`${heading}: ${counts.join(" and ")}.`];
2736
+ for (const item of outcome.items) {
2737
+ if (item.kind === "issue") {
2738
+ const parts = [
2739
+ item.issue.key,
2740
+ item.issue.summary,
2741
+ item.issue.status,
2742
+ item.issue.assignee,
2743
+ item.issue.estimate === void 0 ? void 0 : `estimate ${item.issue.estimate}`,
2744
+ item.issue.url
2745
+ ].filter(
2746
+ (part) => typeof part === "string" && part !== ""
2747
+ );
2748
+ lines.push(`- ${parts.join(" \xB7 ")}`);
2749
+ } else {
2750
+ const parts = [item.page.title, item.page.space, item.page.url].filter(
2751
+ (part) => typeof part === "string" && part !== ""
2752
+ );
2753
+ lines.push(`- ${parts.join(" \xB7 ")}`);
2754
+ }
2755
+ }
2756
+ if (args.jql !== void 0) lines.push(`JQL: ${args.jql}`);
2757
+ if (args.cql !== void 0) lines.push(`CQL: ${args.cql}`);
2758
+ return lines.join("\n");
2759
+ }
2760
+ };
2761
+ var presentTool = new PresentTool();
2762
+
2763
+ // src/host/index.ts
2764
+ var name = PLUGIN_NAME;
2765
+ var inject = ["tools", "credentials", "commands", "authorization"];
2766
+ var OAUTH_METHOD = "oauth";
2767
+ var CLIENT_INFO = { name: "dsh-atlassian", version: "0.3.0" };
2768
+ function apply(ctx, configInput) {
2769
+ const config = resolveConfig(configInput);
2770
+ const profile = atlassianProfile(config);
2771
+ const state = new RuntimeState();
2772
+ const logger = loggerOf(ctx);
2773
+ const store = new GrantStore(ctx, credentialKeyOf(profile));
2774
+ const flow = new AuthorizationFlow(profile, store);
2775
+ const bridge = new McpBridge({
2776
+ profile,
2777
+ store,
2778
+ clientInfo: CLIENT_INFO,
2779
+ // The durable UI channel: the Web Client reads this back as `result.meta`.
2780
+ decorate: (definition, tool) => ({
2781
+ ...definition,
2782
+ output: {
2783
+ ...definition.output,
2784
+ presentationMeta: (args, value) => extractor.extract({
2785
+ tool: tool.name,
2786
+ args,
2787
+ value,
2788
+ siteUrl: state.facts?.siteUrl
2789
+ })
2790
+ }
2791
+ })
2792
+ });
2793
+ const auth = new IntegrationAuth({ ctx, config, state, store, flow, bridge, spec: atlassianAuthSpec });
2794
+ const panel = new PanelApi({ bridge, state });
2795
+ registerDigestSource(ctx, { bridge, state });
2796
+ registerCorrelationSource(ctx, { bridge, state });
2797
+ const disposeFlow = ctx.authorization.registerFlow({
2798
+ key: store.credentialKey,
2799
+ label: "Atlassian",
2800
+ methods: [{ id: OAUTH_METHOD, label: "Sign in with Atlassian" }],
2801
+ run: async (session) => {
2802
+ await flow.run(ctx, session);
2803
+ }
2804
+ });
2805
+ ctx.commands.register({
2806
+ name: COMMAND_NAME,
2807
+ description: "Jira and Confluence: status | login | logout | sync",
2808
+ input: { hint: "status | login | logout | sync" },
2809
+ handler: createCommandHandler(auth)
2810
+ });
2811
+ ctx.tools.register(presentTool.create({ ctx, bridge, state }));
2812
+ mountIntegrationRoute(ctx, {
2813
+ prefix: ROUTE_PREFIX,
2814
+ handler: createApiHandler({ auth, panel })
2815
+ });
2816
+ ctx.effect(
2817
+ () => () => {
2818
+ disposeFlow();
2819
+ return bridge.drop();
2820
+ },
2821
+ "atlassian: flow and MCP bridge"
2822
+ );
2823
+ void auth.connect().then((ready) => {
2824
+ if (!ready) {
2825
+ logger.info(
2826
+ "[atlassian] not signed in yet \u2014 use the Atlassian settings card or /atlassian login"
2827
+ );
2828
+ }
2829
+ });
2830
+ }
2831
+ export {
2832
+ apply,
2833
+ inject,
2834
+ name
2835
+ };
2836
+ //# sourceMappingURL=index.js.map