@sentry/junior-github 0.82.0 → 0.83.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -73,12 +73,81 @@ function permissionCapabilities(permissions) {
73
73
  // src/tools/create-issue.ts
74
74
  import {
75
75
  EgressAuthRequired,
76
- PluginToolInputError
76
+ PluginToolInputError as PluginToolInputError2
77
77
  } from "@sentry/junior-plugin-api";
78
78
  import { Type } from "@sinclair/typebox";
79
79
  import { Value } from "@sinclair/typebox/value";
80
- var GITHUB_ISSUE_FOOTER_START = "<!-- junior-session-footer:start -->";
81
- var GITHUB_ISSUE_FOOTER_END = "<!-- junior-session-footer:end -->";
80
+
81
+ // src/tools/footer.ts
82
+ import { PluginToolInputError } from "@sentry/junior-plugin-api";
83
+ var GITHUB_SESSION_FOOTER_START = "<!-- junior-session-footer:start -->";
84
+ var GITHUB_SESSION_FOOTER_END = "<!-- junior-session-footer:end -->";
85
+ function nonEmptyString(value, name) {
86
+ if (!value?.trim()) {
87
+ throw new PluginToolInputError(`${name} is required`);
88
+ }
89
+ return value.trim();
90
+ }
91
+ function escapeRegExp(value) {
92
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
93
+ }
94
+ function sentryConversationUrl(conversationId) {
95
+ const dsn = process.env.SENTRY_DSN?.trim();
96
+ const orgSlug = process.env.SENTRY_ORG_SLUG?.trim();
97
+ if (!dsn || !orgSlug) {
98
+ return void 0;
99
+ }
100
+ let parsed;
101
+ try {
102
+ parsed = new URL(dsn);
103
+ } catch {
104
+ return void 0;
105
+ }
106
+ const projectId = parsed.pathname.split("/").filter(Boolean).at(-1);
107
+ if (!parsed.hostname || !projectId) {
108
+ return void 0;
109
+ }
110
+ const encodedConversationId = encodeURIComponent(conversationId);
111
+ const params = new URLSearchParams({ project: projectId });
112
+ const path = `explore/conversations/${encodedConversationId}/?${params.toString()}`;
113
+ if (parsed.hostname === "sentry.io" || parsed.hostname.endsWith(".sentry.io")) {
114
+ return `https://${orgSlug}.sentry.io/${path}`;
115
+ }
116
+ const port = parsed.port ? `:${parsed.port}` : "";
117
+ return `${parsed.protocol}//${parsed.hostname}${port}/organizations/${orgSlug}/${path}`;
118
+ }
119
+ function githubConversationFooter(conversationId) {
120
+ const id = nonEmptyString(conversationId, "conversationId");
121
+ const sessionUrl = sentryConversationUrl(id);
122
+ if (!sessionUrl) {
123
+ return void 0;
124
+ }
125
+ return `${GITHUB_SESSION_FOOTER_START}
126
+
127
+ --
128
+
129
+ [View Junior Session in Sentry](${sessionUrl})
130
+
131
+ ${GITHUB_SESSION_FOOTER_END}`;
132
+ }
133
+ function appendGitHubFooter(body, conversationId) {
134
+ const footer = githubConversationFooter(conversationId);
135
+ const normalizedBody = body.trimEnd();
136
+ const existingFooter = new RegExp(
137
+ `${escapeRegExp(GITHUB_SESSION_FOOTER_START)}[\\s\\S]*?${escapeRegExp(GITHUB_SESSION_FOOTER_END)}`
138
+ );
139
+ if (existingFooter.test(normalizedBody)) {
140
+ return footer ? normalizedBody.replace(existingFooter, footer) : normalizedBody.replace(existingFooter, "").trimEnd();
141
+ }
142
+ if (!footer) {
143
+ return normalizedBody;
144
+ }
145
+ return normalizedBody ? `${normalizedBody}
146
+
147
+ ${footer}` : footer;
148
+ }
149
+
150
+ // src/tools/create-issue.ts
82
151
  var GITHUB_ISSUE_CREATE_IDEMPOTENCY_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
83
152
  var GITHUB_ISSUE_CREATE_LOCK_TTL_MS = 6e4;
84
153
  var GitHubIssueCreateRejectedError = class extends Error {
@@ -132,86 +201,28 @@ function parseCreateIssueInput(input) {
132
201
  try {
133
202
  return Value.Parse(createIssueInputSchema, input);
134
203
  } catch (error) {
135
- throw new PluginToolInputError("Invalid GitHub createIssue input.", {
204
+ throw new PluginToolInputError2("Invalid GitHub createIssue input.", {
136
205
  cause: error
137
206
  });
138
207
  }
139
208
  }
140
- function nonEmptyString(value, name) {
209
+ function nonEmptyString2(value, name) {
141
210
  if (!value?.trim()) {
142
- throw new PluginToolInputError(`${name} is required`);
211
+ throw new PluginToolInputError2(`${name} is required`);
143
212
  }
144
213
  return value.trim();
145
214
  }
146
215
  function parseRepo(value) {
147
- const repo = nonEmptyString(value, "repo");
216
+ const repo = nonEmptyString2(value, "repo");
148
217
  const parts = repo.split("/");
149
218
  if (parts.length !== 2 || !parts[0]?.trim() || !parts[1]?.trim()) {
150
- throw new PluginToolInputError('repo must use "owner/name" format');
219
+ throw new PluginToolInputError2('repo must use "owner/name" format');
151
220
  }
152
221
  return {
153
222
  owner: parts[0].trim(),
154
223
  name: parts[1].trim()
155
224
  };
156
225
  }
157
- function escapeRegExp(value) {
158
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
159
- }
160
- function sentryConversationUrl(conversationId) {
161
- const dsn = process.env.SENTRY_DSN?.trim();
162
- const orgSlug = process.env.SENTRY_ORG_SLUG?.trim();
163
- if (!dsn || !orgSlug) {
164
- return void 0;
165
- }
166
- let parsed;
167
- try {
168
- parsed = new URL(dsn);
169
- } catch {
170
- return void 0;
171
- }
172
- const projectId = parsed.pathname.split("/").filter(Boolean).at(-1);
173
- if (!parsed.hostname || !projectId) {
174
- return void 0;
175
- }
176
- const encodedConversationId = encodeURIComponent(conversationId);
177
- const params = new URLSearchParams({ project: projectId });
178
- const path = `explore/conversations/${encodedConversationId}/?${params.toString()}`;
179
- if (parsed.hostname === "sentry.io" || parsed.hostname.endsWith(".sentry.io")) {
180
- return `https://${orgSlug}.sentry.io/${path}`;
181
- }
182
- const port = parsed.port ? `:${parsed.port}` : "";
183
- return `${parsed.protocol}//${parsed.hostname}${port}/organizations/${orgSlug}/${path}`;
184
- }
185
- function githubIssueConversationFooter(conversationId) {
186
- const id = nonEmptyString(conversationId, "conversationId");
187
- const sessionUrl = sentryConversationUrl(id);
188
- if (!sessionUrl) {
189
- return void 0;
190
- }
191
- return `${GITHUB_ISSUE_FOOTER_START}
192
-
193
- [View Session in Sentry](${sessionUrl})
194
-
195
- ${GITHUB_ISSUE_FOOTER_END}`;
196
- }
197
- function appendGitHubIssueFooter(body, conversationId) {
198
- const footer = githubIssueConversationFooter(conversationId);
199
- const normalizedBody = body.trimEnd();
200
- const existingFooter = new RegExp(
201
- `${escapeRegExp(GITHUB_ISSUE_FOOTER_START)}[\\s\\S]*?${escapeRegExp(
202
- GITHUB_ISSUE_FOOTER_END
203
- )}`
204
- );
205
- if (existingFooter.test(normalizedBody)) {
206
- return footer ? normalizedBody.replace(existingFooter, footer) : normalizedBody.replace(existingFooter, "").trimEnd();
207
- }
208
- if (!footer) {
209
- return normalizedBody;
210
- }
211
- return normalizedBody ? `${normalizedBody}
212
-
213
- ${footer}` : footer;
214
- }
215
226
  async function readJsonResponse(response) {
216
227
  const text = await response.text();
217
228
  if (!text) {
@@ -259,11 +270,11 @@ function isDefinitiveGitHubIssueCreateRejection(error) {
259
270
  function createGitHubIssueRequest(conversationId, input) {
260
271
  const repo = parseRepo(input.repo);
261
272
  const labels = input.labels?.map(
262
- (label) => nonEmptyString(label, "labels entry")
273
+ (label) => nonEmptyString2(label, "labels entry")
263
274
  );
264
275
  const payload = {
265
- title: nonEmptyString(input.title, "title"),
266
- body: appendGitHubIssueFooter(input.body ?? "", conversationId),
276
+ title: nonEmptyString2(input.title, "title"),
277
+ body: appendGitHubFooter(input.body ?? "", conversationId),
267
278
  ...labels?.length ? { labels } : {}
268
279
  };
269
280
  return new Request(
@@ -314,11 +325,11 @@ function createGitHubIssueTool(ctx) {
314
325
  inputSchema: createIssueInputSchema,
315
326
  async execute(input, options) {
316
327
  const parsedInput = parseCreateIssueInput(input);
317
- const conversationId = nonEmptyString(
328
+ const conversationId = nonEmptyString2(
318
329
  ctx.conversationId,
319
330
  "conversationId"
320
331
  );
321
- const toolCallId = nonEmptyString(options?.toolCallId, "toolCallId");
332
+ const toolCallId = nonEmptyString2(options?.toolCallId, "toolCallId");
322
333
  const key = `createIssue:${conversationId}:${toolCallId}`;
323
334
  return await ctx.state.withLock(
324
335
  `${key}:lock`,
@@ -377,12 +388,10 @@ function createGitHubIssueTool(ctx) {
377
388
  // src/tools/create-pull-request.ts
378
389
  import {
379
390
  EgressAuthRequired as EgressAuthRequired2,
380
- PluginToolInputError as PluginToolInputError2
391
+ PluginToolInputError as PluginToolInputError3
381
392
  } from "@sentry/junior-plugin-api";
382
393
  import { Type as Type2 } from "@sinclair/typebox";
383
394
  import { Value as Value2 } from "@sinclair/typebox/value";
384
- var GITHUB_PULL_REQUEST_FOOTER_START = "<!-- junior-session-footer:start -->";
385
- var GITHUB_PULL_REQUEST_FOOTER_END = "<!-- junior-session-footer:end -->";
386
395
  var GITHUB_PULL_REQUEST_CREATE_IDEMPOTENCY_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
387
396
  var GITHUB_PULL_REQUEST_CREATE_LOCK_TTL_MS = 6e4;
388
397
  var GitHubPullRequestCreateRejectedError = class extends Error {
@@ -424,6 +433,7 @@ var createPullRequestStateSchema = Type2.Union([
424
433
  Type2.Object(
425
434
  {
426
435
  createdAtMs: Type2.Number(),
436
+ input: Type2.Optional(createPullRequestInputSchema),
427
437
  number: Type2.Number(),
428
438
  status: Type2.Literal("completed"),
429
439
  url: Type2.String()
@@ -433,6 +443,7 @@ var createPullRequestStateSchema = Type2.Union([
433
443
  Type2.Object(
434
444
  {
435
445
  createdAtMs: Type2.Number(),
446
+ input: Type2.Optional(createPullRequestInputSchema),
436
447
  status: Type2.Literal("pending")
437
448
  },
438
449
  { additionalProperties: false }
@@ -442,86 +453,28 @@ function parseCreatePullRequestInput(input) {
442
453
  try {
443
454
  return Value2.Parse(createPullRequestInputSchema, input);
444
455
  } catch (error) {
445
- throw new PluginToolInputError2("Invalid GitHub createPullRequest input.", {
456
+ throw new PluginToolInputError3("Invalid GitHub createPullRequest input.", {
446
457
  cause: error
447
458
  });
448
459
  }
449
460
  }
450
- function nonEmptyString2(value, name) {
461
+ function nonEmptyString3(value, name) {
451
462
  if (!value?.trim()) {
452
- throw new PluginToolInputError2(`${name} is required`);
463
+ throw new PluginToolInputError3(`${name} is required`);
453
464
  }
454
465
  return value.trim();
455
466
  }
456
467
  function parseRepo2(value) {
457
- const repo = nonEmptyString2(value, "repo");
468
+ const repo = nonEmptyString3(value, "repo");
458
469
  const parts = repo.split("/");
459
470
  if (parts.length !== 2 || !parts[0]?.trim() || !parts[1]?.trim()) {
460
- throw new PluginToolInputError2('repo must use "owner/name" format');
471
+ throw new PluginToolInputError3('repo must use "owner/name" format');
461
472
  }
462
473
  return {
463
474
  owner: parts[0].trim(),
464
475
  name: parts[1].trim()
465
476
  };
466
477
  }
467
- function escapeRegExp2(value) {
468
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
469
- }
470
- function sentryConversationUrl2(conversationId) {
471
- const dsn = process.env.SENTRY_DSN?.trim();
472
- const orgSlug = process.env.SENTRY_ORG_SLUG?.trim();
473
- if (!dsn || !orgSlug) {
474
- return void 0;
475
- }
476
- let parsed;
477
- try {
478
- parsed = new URL(dsn);
479
- } catch {
480
- return void 0;
481
- }
482
- const projectId = parsed.pathname.split("/").filter(Boolean).at(-1);
483
- if (!parsed.hostname || !projectId) {
484
- return void 0;
485
- }
486
- const encodedConversationId = encodeURIComponent(conversationId);
487
- const params = new URLSearchParams({ project: projectId });
488
- const path = `explore/conversations/${encodedConversationId}/?${params.toString()}`;
489
- if (parsed.hostname === "sentry.io" || parsed.hostname.endsWith(".sentry.io")) {
490
- return `https://${orgSlug}.sentry.io/${path}`;
491
- }
492
- const port = parsed.port ? `:${parsed.port}` : "";
493
- return `${parsed.protocol}//${parsed.hostname}${port}/organizations/${orgSlug}/${path}`;
494
- }
495
- function githubPullRequestConversationFooter(conversationId) {
496
- const id = nonEmptyString2(conversationId, "conversationId");
497
- const sessionUrl = sentryConversationUrl2(id);
498
- if (!sessionUrl) {
499
- return void 0;
500
- }
501
- return `${GITHUB_PULL_REQUEST_FOOTER_START}
502
-
503
- [View Session in Sentry](${sessionUrl})
504
-
505
- ${GITHUB_PULL_REQUEST_FOOTER_END}`;
506
- }
507
- function appendGitHubPullRequestFooter(body, conversationId) {
508
- const footer = githubPullRequestConversationFooter(conversationId);
509
- const normalizedBody = body.trimEnd();
510
- const existingFooter = new RegExp(
511
- `${escapeRegExp2(GITHUB_PULL_REQUEST_FOOTER_START)}[\\s\\S]*?${escapeRegExp2(
512
- GITHUB_PULL_REQUEST_FOOTER_END
513
- )}`
514
- );
515
- if (existingFooter.test(normalizedBody)) {
516
- return footer ? normalizedBody.replace(existingFooter, footer) : normalizedBody.replace(existingFooter, "").trimEnd();
517
- }
518
- if (!footer) {
519
- return normalizedBody;
520
- }
521
- return normalizedBody ? `${normalizedBody}
522
-
523
- ${footer}` : footer;
524
- }
525
478
  async function readJsonResponse2(response) {
526
479
  const text = await response.text();
527
480
  if (!text) {
@@ -569,10 +522,10 @@ function isDefinitiveGitHubPullRequestCreateRejection(error) {
569
522
  function createGitHubPullRequestRequest(conversationId, input) {
570
523
  const repo = parseRepo2(input.repo);
571
524
  const payload = {
572
- title: nonEmptyString2(input.title, "title"),
573
- head: nonEmptyString2(input.head, "head"),
574
- base: nonEmptyString2(input.base, "base"),
575
- body: appendGitHubPullRequestFooter(input.body ?? "", conversationId),
525
+ title: nonEmptyString3(input.title, "title"),
526
+ head: nonEmptyString3(input.head, "head"),
527
+ base: nonEmptyString3(input.base, "base"),
528
+ body: appendGitHubFooter(input.body ?? "", conversationId),
576
529
  ...input.draft !== void 0 ? { draft: input.draft } : {}
577
530
  };
578
531
  return new Request(
@@ -621,17 +574,51 @@ async function createGitHubPullRequest(ctx, request) {
621
574
  url: pullRequest.html_url
622
575
  };
623
576
  }
577
+ function gitHubPullRequestSubscribable(input, result) {
578
+ const repo = parseRepo2(input.repo);
579
+ const repoRef = `${repo.owner}/${repo.name}`;
580
+ const supportedEvents = [
581
+ "checks.failed",
582
+ "checks.recovered",
583
+ "review.approved",
584
+ "review.changes_requested",
585
+ "state.merged",
586
+ "state.closed_unmerged"
587
+ ];
588
+ return {
589
+ label: `GitHub PR ${repoRef}#${result.number}`,
590
+ provider: "github",
591
+ resourceRef: `github:pull_request:${repoRef}#${result.number}`,
592
+ suggestedEvents: [
593
+ "checks.failed",
594
+ "review.changes_requested",
595
+ "state.merged",
596
+ "state.closed_unmerged"
597
+ ],
598
+ supportedEvents,
599
+ type: "pull_request"
600
+ };
601
+ }
602
+ function gitHubPullRequestToolResult(input, result) {
603
+ if (!process.env.GITHUB_WEBHOOK_SECRET?.trim()) {
604
+ return result;
605
+ }
606
+ return {
607
+ ...result,
608
+ subscribable: gitHubPullRequestSubscribable(input, result)
609
+ };
610
+ }
624
611
  function createGitHubPullRequestTool(ctx) {
625
612
  return {
626
613
  description: "Create a GitHub pull request with a runtime-owned Junior conversation footer. Use this instead of shelling out to gh pr create when creating pull requests.",
627
614
  inputSchema: createPullRequestInputSchema,
628
615
  async execute(input, options) {
629
616
  const parsedInput = parseCreatePullRequestInput(input);
630
- const conversationId = nonEmptyString2(
617
+ const conversationId = nonEmptyString3(
631
618
  ctx.conversationId,
632
619
  "conversationId"
633
620
  );
634
- const toolCallId = nonEmptyString2(options?.toolCallId, "toolCallId");
621
+ const toolCallId = nonEmptyString3(options?.toolCallId, "toolCallId");
635
622
  const key = `createPullRequest:${conversationId}:${toolCallId}`;
636
623
  return await ctx.state.withLock(
637
624
  `${key}:lock`,
@@ -639,10 +626,10 @@ function createGitHubPullRequestTool(ctx) {
639
626
  async () => {
640
627
  const state = createPullRequestState(await ctx.state.get(key));
641
628
  if (state?.status === "completed") {
642
- return {
629
+ return gitHubPullRequestToolResult(state.input ?? parsedInput, {
643
630
  number: state.number,
644
631
  url: state.url
645
- };
632
+ });
646
633
  }
647
634
  if (state?.status === "pending") {
648
635
  throw new Error(
@@ -655,7 +642,8 @@ function createGitHubPullRequestTool(ctx) {
655
642
  );
656
643
  const pendingState = {
657
644
  status: "pending",
658
- createdAtMs: Date.now()
645
+ createdAtMs: Date.now(),
646
+ input: parsedInput
659
647
  };
660
648
  await ctx.state.set(
661
649
  key,
@@ -677,7 +665,7 @@ function createGitHubPullRequestTool(ctx) {
677
665
  { cause: error }
678
666
  );
679
667
  }
680
- return result;
668
+ return gitHubPullRequestToolResult(parsedInput, result);
681
669
  } catch (error) {
682
670
  if (isEgressAuthRequired2(error) || isDefinitiveGitHubPullRequestCreateRejection(error)) {
683
671
  await ctx.state.delete(key);
@@ -1359,7 +1347,7 @@ function parseGitHubGraphqlOperation(bodyText) {
1359
1347
  if (operationName) {
1360
1348
  const namedOperation = normalized.match(
1361
1349
  new RegExp(
1362
- `\\b(query|mutation|subscription)\\s+${escapeRegExp3(operationName)}\\b`
1350
+ `\\b(query|mutation|subscription)\\s+${escapeRegExp2(operationName)}\\b`
1363
1351
  )
1364
1352
  )?.[1];
1365
1353
  return namedOperation ? graphqlOperationAccess(namedOperation) : void 0;
@@ -1400,7 +1388,7 @@ function parseGitHubGraphqlRequest(bodyText) {
1400
1388
  ...operationName ? { operationName } : {}
1401
1389
  };
1402
1390
  }
1403
- function escapeRegExp3(value) {
1391
+ function escapeRegExp2(value) {
1404
1392
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1405
1393
  }
1406
1394
  function graphqlOperationAccess(operation) {
@@ -1529,7 +1517,7 @@ function isGitHubIssueCreateGraphqlMutation(method, upstreamUrl, bodyText) {
1529
1517
  return /\bmutation\b/.test(parsed.normalized);
1530
1518
  }
1531
1519
  return new RegExp(
1532
- `\\bmutation\\s+${escapeRegExp3(parsed.operationName)}\\b`
1520
+ `\\bmutation\\s+${escapeRegExp2(parsed.operationName)}\\b`
1533
1521
  ).test(parsed.normalized);
1534
1522
  }
1535
1523
  function isGitHubPullCreateGraphqlMutation(method, upstreamUrl, bodyText) {
@@ -1547,7 +1535,7 @@ function isGitHubPullCreateGraphqlMutation(method, upstreamUrl, bodyText) {
1547
1535
  return /\bmutation\b/.test(parsed.normalized);
1548
1536
  }
1549
1537
  return new RegExp(
1550
- `\\bmutation\\s+${escapeRegExp3(parsed.operationName)}\\b`
1538
+ `\\bmutation\\s+${escapeRegExp2(parsed.operationName)}\\b`
1551
1539
  ).test(parsed.normalized);
1552
1540
  }
1553
1541
  function assertGitHubWriteAllowed(input) {
@@ -0,0 +1,13 @@
1
+ export declare const GITHUB_SESSION_FOOTER_START = "<!-- junior-session-footer:start -->";
2
+ export declare const GITHUB_SESSION_FOOTER_END = "<!-- junior-session-footer:end -->";
3
+ export declare function sentryConversationUrl(conversationId: string): string | undefined;
4
+ /**
5
+ * Build the Junior session footer block for GitHub issues and pull requests.
6
+ * Returns undefined when Sentry DSN/org are not configured.
7
+ */
8
+ export declare function githubConversationFooter(conversationId: string): string | undefined;
9
+ /**
10
+ * Append (or replace an existing) Junior session footer to a GitHub body string.
11
+ * When Sentry is not configured, returns the body unchanged (existing footer stripped).
12
+ */
13
+ export declare function appendGitHubFooter(body: string, conversationId: string): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-github",
3
- "version": "0.82.0",
3
+ "version": "0.83.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "dependencies": {
26
26
  "@sinclair/typebox": "^0.34.49",
27
- "@sentry/junior-plugin-api": "0.82.0"
27
+ "@sentry/junior-plugin-api": "0.83.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/node": "^25.9.1",