@sentry/junior-google 0.242.0 → 0.243.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/README.md CHANGED
@@ -4,7 +4,8 @@ Proof of concept. Junior acts as its own Google Workspace account, for example
4
4
  `junior@sentry.io`. Junior does not use the Google credentials of the people it
5
5
  helps. Tracking issue: getsentry/junior#2063.
6
6
 
7
- This version supports Calendar only. Drive and Gmail are out of scope.
7
+ This version supports Calendar and reading Google Docs. Gmail and Drive
8
+ writes are out of scope.
8
9
 
9
10
  ## Surfaces
10
11
 
@@ -41,6 +42,17 @@ This version supports Calendar only. Drive and Gmail are out of scope.
41
42
  annotation on the Conversation. Junior shows its card with the next reply.
42
43
  The card shows the title, time, and attendees, and links to the event. A
43
44
  change to an event replaces its card.
45
+ - `searchGoogleDocs` finds Google Docs that are shared with Junior's account,
46
+ by words in the title or text. Docs that are only shared by public link are
47
+ not found, because Drive search only covers files the account has.
48
+ - `readGoogleDoc` reads one Doc as Markdown, from a link or a document id. It
49
+ works for Docs shared with Junior's account and for Docs shared with anyone
50
+ who has the link. Drive exports the Doc, so the Docs API is not used. A
51
+ link's `resourcekey` value is sent to Google, because older Docs shared by
52
+ link need it. Results stop at 100,000 characters.
53
+ - `listGoogleDocComments` reads the comments and replies on one Doc, 100 per
54
+ call. Each comment gives its author name, text, the Doc text it is attached
55
+ to, and whether it is resolved.
44
56
  - The **Google account** page on the dashboard Admin page shows whether the
45
57
  account is connected. Its Connect button starts Google sign-in.
46
58
  - `junior google connect` and `junior google status` are the operator CLI.
@@ -57,20 +69,21 @@ own.
57
69
  2. Google sign-in opens with PKCE and a single-use state value.
58
70
  3. The admin signs in as the configured account, not as themselves.
59
71
  4. Junior checks that the identity token names the configured account. It also
60
- checks that Google granted every Calendar scope. If a check fails, Junior
61
- revokes the grant and stores nothing.
72
+ checks that Google granted every Calendar and Docs scope. If a check
73
+ fails, Junior revokes the grant and stores nothing.
62
74
  5. Junior stores the refresh token in the `junior_google_accounts` table.
63
75
 
64
76
  At runtime, the `issueCredential` hook exchanges the refresh token for a
65
77
  one-hour access token. Junior applies that token only to `www.googleapis.com`
66
78
  requests from this plugin's tools. The `grantForEgress` hook denies sandbox
67
79
  commands and any request that does not match a declared operation. Thus the
68
- model cannot use `curl` to read Junior's calendar and skip the tool rules.
80
+ model cannot use `curl` to read Junior's calendar or Drive and skip the tool
81
+ rules.
69
82
 
70
83
  If Google rejects the refresh token (`invalid_grant`), Junior deletes it. The
71
84
  tools then report that an admin must connect the account again. If the stored
72
- grant does not have every scope the current tools need, the tools also ask an
73
- admin to reconnect.
85
+ grant does not have every scope the current tools need, Google rejects the
86
+ request and the tool asks an admin to reconnect.
74
87
 
75
88
  ## Rules in code
76
89
 
@@ -81,8 +94,11 @@ admin to reconnect.
81
94
  requester who is invited to the event. The `calendar.events.owned` scope
82
95
  also keeps events that other people organize out of reach.
83
96
  - Calendar scopes are `calendar.events.freebusy`, `calendar.events.readonly`,
84
- and `calendar.events.owned`. Junior also requests `openid email` to verify
85
- the account during sign-in.
97
+ and `calendar.events.owned`.
98
+ - The Docs scope is `drive.readonly`. The Drive comments API accepts no
99
+ narrower read scope. Junior permits only these Drive requests: file search,
100
+ file metadata, export, and comment reads.
101
+ - Junior also requests `openid email` to verify the account during sign-in.
86
102
 
87
103
  ## Setup
88
104
 
@@ -91,9 +107,10 @@ sign-in requests only its own scopes, so dashboard sign-in still asks only for
91
107
  identity.
92
108
 
93
109
  1. In the Google Cloud project of the dashboard OAuth client, enable the
94
- Google Calendar API.
110
+ Google Calendar API and the Google Drive API.
95
111
  2. On the OAuth consent screen, use user type **Internal** and add the
96
- Calendar scopes above.
112
+ Calendar and Docs scopes above. `drive.readonly` is a restricted scope.
113
+ An Internal app does not need Google verification for it.
97
114
  3. On the OAuth client, add these redirect URIs:
98
115
  - `https://<junior host>/api/plugins/google/oauth/callback`
99
116
  - `http://127.0.0.1:8765/oauth/callback`, for the CLI. Use another port with
@@ -126,6 +143,11 @@ variables are set. The Admin page shows **Not configured** until then.
126
143
  - Anyone who can talk to Junior can ask it to read a colleague's calendar.
127
144
  Junior returns what its own account can see, so set the sharing default
128
145
  with that in mind.
146
+ - Anyone who can talk to Junior can also ask it to read any Doc shared with
147
+ Junior's account. Share a Doc with that account only when everyone who can
148
+ talk to Junior may read it.
149
+ - `drive.readonly` lets the account read every Drive file it can open, not
150
+ only Docs. The tools read Google Docs only.
129
151
  - Google does not expose the working hours that people set in Calendar.
130
152
  Junior uses each calendar's time zone and one shared workday instead.
131
153
  - Junior cannot find or book meeting rooms. Room discovery needs the Admin
package/dist/config.d.ts CHANGED
@@ -9,7 +9,8 @@ export declare const GOOGLE_CLIENT_SECRET_ENV = "GOOGLE_CLIENT_SECRET";
9
9
  export declare const GOOGLE_ACCOUNT_EMAIL_ENV = "GOOGLE_WORKSPACE_ACCOUNT_EMAIL";
10
10
  export declare const GOOGLE_ALLOWED_DOMAINS_ENV = "GOOGLE_WORKSPACE_ALLOWED_DOMAINS";
11
11
  /**
12
- * Scopes the Calendar tools need. Request nothing broader.
12
+ * API scopes the tools need. A connected grant must have all of them. Request
13
+ * nothing broader.
13
14
  *
14
15
  * - `calendar.events.freebusy`: availability for `findMeetingTimes`.
15
16
  * - `calendar.events.readonly`: events that Junior's account can see on other
@@ -17,8 +18,10 @@ export declare const GOOGLE_ALLOWED_DOMAINS_ENV = "GOOGLE_WORKSPACE_ALLOWED_DOMA
17
18
  * still decide how much detail each calendar shows.
18
19
  * - `calendar.events.owned`: create and change events on Junior's own
19
20
  * calendar.
21
+ * - `drive.readonly`: search, export, and read comments on Docs. The Drive
22
+ * comments API accepts no narrower read scope.
20
23
  */
21
- export declare const GOOGLE_CALENDAR_SCOPES: readonly ["https://www.googleapis.com/auth/calendar.events.freebusy", "https://www.googleapis.com/auth/calendar.events.owned", "https://www.googleapis.com/auth/calendar.events.readonly"];
24
+ export declare const GOOGLE_API_SCOPES: readonly ["https://www.googleapis.com/auth/calendar.events.freebusy", "https://www.googleapis.com/auth/calendar.events.owned", "https://www.googleapis.com/auth/calendar.events.readonly", "https://www.googleapis.com/auth/drive.readonly"];
22
25
  /** Identity scopes used only to verify which account signed in. */
23
26
  export declare const GOOGLE_IDENTITY_SCOPES: readonly ["openid", "email"];
24
27
  export interface GoogleConfig {
@@ -39,6 +39,26 @@ export declare const GOOGLE_OPERATIONS: {
39
39
  readonly method: "POST";
40
40
  readonly path: RegExp;
41
41
  };
42
+ readonly "google.drive.comments.list": {
43
+ readonly access: "read";
44
+ readonly method: "GET";
45
+ readonly path: RegExp;
46
+ };
47
+ readonly "google.drive.file.export": {
48
+ readonly access: "read";
49
+ readonly method: "GET";
50
+ readonly path: RegExp;
51
+ };
52
+ readonly "google.drive.file.get": {
53
+ readonly access: "read";
54
+ readonly method: "GET";
55
+ readonly path: RegExp;
56
+ };
57
+ readonly "google.drive.files.list": {
58
+ readonly access: "read";
59
+ readonly method: "GET";
60
+ readonly path: RegExp;
61
+ };
42
62
  };
43
63
  export type GoogleOperation = keyof typeof GOOGLE_OPERATIONS;
44
64
  /** Select the grant for one Google request, or deny it. */
package/dist/index.d.ts CHANGED
@@ -3,7 +3,8 @@
3
3
  *
4
4
  * Junior acts as its own Google Workspace account. A Junior admin connects
5
5
  * that account out of band, through the dashboard Admin page or the CLI. Calendar
6
- * tools use host-owned egress, and the sandbox gets no Google credential.
6
+ * and Docs tools use host-owned egress, and the sandbox gets no Google
7
+ * credential.
7
8
  */
8
9
  import { type PluginRegistration } from "@sentry/junior-plugin-api";
9
10
  /** Register Junior's Google Workspace plugin. */
package/dist/index.js CHANGED
@@ -13,10 +13,11 @@ var GOOGLE_CLIENT_ID_ENV = "GOOGLE_CLIENT_ID";
13
13
  var GOOGLE_CLIENT_SECRET_ENV = "GOOGLE_CLIENT_SECRET";
14
14
  var GOOGLE_ACCOUNT_EMAIL_ENV = "GOOGLE_WORKSPACE_ACCOUNT_EMAIL";
15
15
  var GOOGLE_ALLOWED_DOMAINS_ENV = "GOOGLE_WORKSPACE_ALLOWED_DOMAINS";
16
- var GOOGLE_CALENDAR_SCOPES = [
16
+ var GOOGLE_API_SCOPES = [
17
17
  "https://www.googleapis.com/auth/calendar.events.freebusy",
18
18
  "https://www.googleapis.com/auth/calendar.events.owned",
19
- "https://www.googleapis.com/auth/calendar.events.readonly"
19
+ "https://www.googleapis.com/auth/calendar.events.readonly",
20
+ "https://www.googleapis.com/auth/drive.readonly"
20
21
  ];
21
22
  var GOOGLE_IDENTITY_SCOPES = ["openid", "email"];
22
23
  function readEnv(name) {
@@ -147,7 +148,7 @@ function googleAuthorizationUrl(input) {
147
148
  prompt: "consent select_account",
148
149
  redirect_uri: input.redirectUri,
149
150
  response_type: "code",
150
- scope: [...GOOGLE_IDENTITY_SCOPES, ...GOOGLE_CALENDAR_SCOPES].join(" "),
151
+ scope: [...GOOGLE_IDENTITY_SCOPES, ...GOOGLE_API_SCOPES].join(" "),
151
152
  state: input.request.state
152
153
  }).toString();
153
154
  return url.toString();
@@ -193,10 +194,10 @@ function verifyGrant(tokens, config) {
193
194
  };
194
195
  }
195
196
  const granted = new Set(grantedScopes(tokens));
196
- const missing = GOOGLE_CALENDAR_SCOPES.filter((scope) => !granted.has(scope));
197
+ const missing = GOOGLE_API_SCOPES.filter((scope) => !granted.has(scope));
197
198
  if (missing.length > 0) {
198
199
  return {
199
- reason: `Google did not grant every Calendar permission. Start again and allow all requested access. Missing: ${missing.join(", ")}`
200
+ reason: `Google did not grant every requested permission. Start again and allow all requested access. Missing: ${missing.join(", ")}`
200
201
  };
201
202
  }
202
203
  if (!tokens.refresh_token) {
@@ -465,6 +466,26 @@ var GOOGLE_OPERATIONS = {
465
466
  access: "read",
466
467
  method: "POST",
467
468
  path: /^\/calendar\/v3\/freeBusy$/
469
+ },
470
+ "google.drive.comments.list": {
471
+ access: "read",
472
+ method: "GET",
473
+ path: /^\/drive\/v3\/files\/[A-Za-z0-9_-]+\/comments$/
474
+ },
475
+ "google.drive.file.export": {
476
+ access: "read",
477
+ method: "GET",
478
+ path: /^\/drive\/v3\/files\/[A-Za-z0-9_-]+\/export$/
479
+ },
480
+ "google.drive.file.get": {
481
+ access: "read",
482
+ method: "GET",
483
+ path: /^\/drive\/v3\/files\/[A-Za-z0-9_-]+$/
484
+ },
485
+ "google.drive.files.list": {
486
+ access: "read",
487
+ method: "GET",
488
+ path: /^\/drive\/v3\/files$/
468
489
  }
469
490
  };
470
491
  function googleGrantForEgress(ctx) {
@@ -776,7 +797,10 @@ async function withRequester(ctx, emails) {
776
797
  return [...new Set(all)];
777
798
  }
778
799
  var googleErrorSchema = z2.object({
779
- error: z2.object({ message: z2.string().optional() }).optional()
800
+ error: z2.object({
801
+ details: z2.array(z2.object({ reason: z2.string().optional() })).optional(),
802
+ message: z2.string().optional()
803
+ }).optional()
780
804
  });
781
805
  async function googleApiRequest(ctx, input) {
782
806
  const url = new URL(`https://${GOOGLE_API_DOMAIN}${input.path}`);
@@ -790,17 +814,26 @@ async function googleApiRequest(ctx, input) {
790
814
  method: GOOGLE_OPERATIONS[input.operation].method,
791
815
  headers: {
792
816
  Accept: "application/json",
793
- "Content-Type": "application/json"
817
+ "Content-Type": "application/json",
818
+ ...input.headers
794
819
  },
795
820
  body: input.body === void 0 ? void 0 : JSON.stringify(input.body)
796
821
  })
797
822
  });
798
- const body = await response.json().catch(() => void 0);
823
+ const body = response.headers.get("content-type")?.includes("json") ? await response.json().catch(() => void 0) : await response.text() || void 0;
799
824
  return { body, status: response.status };
800
825
  }
801
826
  function googleApiError(operation, response) {
802
827
  const parsed = googleErrorSchema.safeParse(response.body);
803
- const detail = parsed.success ? parsed.data.error?.message : void 0;
828
+ const error = parsed.success ? parsed.data.error : void 0;
829
+ if (response.status === 403 && error?.details?.some(
830
+ (detail2) => detail2.reason === "ACCESS_TOKEN_SCOPE_INSUFFICIENT"
831
+ )) {
832
+ return new Error(
833
+ `Junior's Google account does not have the permission that ${operation} needs. A Junior admin must reconnect it on the dashboard Admin page or with \`junior google connect\`.`
834
+ );
835
+ }
836
+ const detail = error?.message;
804
837
  const message = `Google ${operation} failed with HTTP ${response.status}${detail ? `: ${detail}` : ""}`;
805
838
  return response.status === 400 ? new PluginToolInputError(message) : new Error(message);
806
839
  }
@@ -1497,72 +1530,246 @@ function createFindMeetingTimesTool(ctx) {
1497
1530
  });
1498
1531
  }
1499
1532
 
1500
- // src/tools/list-events.ts
1533
+ // src/tools/list-doc-comments.ts
1501
1534
  import {
1502
1535
  definePluginTool as definePluginTool4,
1503
- PluginToolInputError as PluginToolInputError4,
1504
1536
  pluginToolOutputSchema as pluginToolOutputSchema4
1505
1537
  } from "@sentry/junior-plugin-api";
1538
+ import { z as z7 } from "zod";
1539
+
1540
+ // src/tools/docs.ts
1541
+ import { PluginToolInputError as PluginToolInputError4 } from "@sentry/junior-plugin-api";
1506
1542
  import { z as z6 } from "zod";
1543
+ var GOOGLE_DOC_MIME_TYPE = "application/vnd.google-apps.document";
1544
+ var docReferenceSchema = z6.string().trim().min(10).max(2048).describe(
1545
+ "Google Docs link, or a document id from searchGoogleDocs. Keep the full link for Docs shared by link: it can carry a resource key that Google needs."
1546
+ );
1547
+ var FILE_ID_PATTERN = /^[A-Za-z0-9_-]{10,200}$/;
1548
+ function parseDocReference(value) {
1549
+ let fileId = value;
1550
+ let resourceKey;
1551
+ if (/^https?:\/\//.test(value)) {
1552
+ const url = URL.parse(value);
1553
+ fileId = url?.pathname.match(/\/d\/([^/]+)/)?.[1] ?? url?.searchParams.get("id") ?? "";
1554
+ resourceKey = url?.searchParams.get("resourcekey") ?? void 0;
1555
+ }
1556
+ if (!FILE_ID_PATTERN.test(fileId)) {
1557
+ throw new PluginToolInputError4(
1558
+ "Use a Google Docs link or a document id from searchGoogleDocs."
1559
+ );
1560
+ }
1561
+ return { fileId, ...resourceKey ? { resourceKey } : void 0 };
1562
+ }
1563
+ async function driveFileRequest(ctx, doc, input) {
1564
+ const response = await googleApiRequest(ctx, {
1565
+ ...input,
1566
+ headers: {
1567
+ ...input.headers,
1568
+ ...doc.resourceKey ? {
1569
+ "X-Goog-Drive-Resource-Keys": `${doc.fileId}/${doc.resourceKey}`
1570
+ } : void 0
1571
+ }
1572
+ });
1573
+ if (response.status === 404) {
1574
+ throw new PluginToolInputError4(
1575
+ `Junior's Google account (${ctx.accountEmail}) cannot open this Doc. Ask the owner to share it with that account, or to share it with anyone who has the link.`
1576
+ );
1577
+ }
1578
+ if (response.status !== 200) {
1579
+ throw googleApiError(input.operation, response);
1580
+ }
1581
+ return response;
1582
+ }
1583
+ var docFileSchema = z6.object({
1584
+ id: z6.string(),
1585
+ mimeType: z6.string(),
1586
+ modifiedTime: z6.string().optional(),
1587
+ name: z6.string(),
1588
+ webViewLink: z6.string().optional()
1589
+ });
1590
+ async function readDocFile(ctx, doc) {
1591
+ const response = await driveFileRequest(ctx, doc, {
1592
+ operation: "google.drive.file.get",
1593
+ path: `/drive/v3/files/${doc.fileId}`,
1594
+ query: {
1595
+ fields: "id,mimeType,modifiedTime,name,webViewLink",
1596
+ supportsAllDrives: "true"
1597
+ }
1598
+ });
1599
+ const file = docFileSchema.parse(response.body);
1600
+ if (file.mimeType !== GOOGLE_DOC_MIME_TYPE) {
1601
+ throw new PluginToolInputError4(
1602
+ `"${file.name}" is not a Google Doc (${file.mimeType}). Junior can only read Google Docs.`
1603
+ );
1604
+ }
1605
+ return file;
1606
+ }
1607
+
1608
+ // src/tools/list-doc-comments.ts
1609
+ var inputSchema4 = z7.object({
1610
+ document: docReferenceSchema,
1611
+ pageToken: z7.string().min(1).max(2048).optional().describe("nextPageToken from an earlier call, to read more comments.")
1612
+ }).strict();
1613
+ var replyOutputSchema = z7.object({
1614
+ action: z7.enum(["resolve", "reopen"]).optional().describe("Set when the reply resolved or reopened the comment."),
1615
+ author: z7.string().optional(),
1616
+ content: z7.string(),
1617
+ createdTime: z7.string().optional()
1618
+ });
1619
+ var commentOutputSchema = z7.object({
1620
+ author: z7.string().optional(),
1621
+ commentId: z7.string(),
1622
+ content: z7.string(),
1623
+ createdTime: z7.string().optional(),
1624
+ quotedText: z7.string().optional().describe("The Doc text that the comment is attached to."),
1625
+ replies: z7.array(replyOutputSchema),
1626
+ resolved: z7.boolean()
1627
+ });
1628
+ var outputSchema4 = pluginToolOutputSchema4.extend({
1629
+ target: z7.literal("listGoogleDocComments"),
1630
+ comments: z7.array(commentOutputSchema),
1631
+ documentId: z7.string(),
1632
+ nextPageToken: z7.string().optional().describe("Set when the Doc has more comments. Pass it as pageToken.")
1633
+ });
1634
+ var authorSchema = z7.object({ displayName: z7.string().optional() }).optional();
1635
+ var commentListSchema = z7.object({
1636
+ comments: z7.array(
1637
+ z7.object({
1638
+ author: authorSchema,
1639
+ content: z7.string().default(""),
1640
+ createdTime: z7.string().optional(),
1641
+ id: z7.string(),
1642
+ quotedFileContent: z7.object({ value: z7.string().optional() }).optional(),
1643
+ replies: z7.array(
1644
+ z7.object({
1645
+ // Google may add actions; keep only the known ones.
1646
+ action: z7.enum(["resolve", "reopen"]).optional().catch(void 0),
1647
+ author: authorSchema,
1648
+ content: z7.string().default(""),
1649
+ createdTime: z7.string().optional()
1650
+ })
1651
+ ).default([]),
1652
+ resolved: z7.boolean().optional()
1653
+ })
1654
+ ).default([]),
1655
+ nextPageToken: z7.string().optional()
1656
+ });
1657
+ var COMMENT_FIELDS = "comments(author/displayName,content,createdTime,id,quotedFileContent/value,resolved,replies(action,author/displayName,content,createdTime)),nextPageToken";
1658
+ function createListGoogleDocCommentsTool(ctx) {
1659
+ return definePluginTool4({
1660
+ annotations: {
1661
+ destructiveHint: false,
1662
+ idempotentHint: true,
1663
+ openWorldHint: true,
1664
+ readOnlyHint: true
1665
+ },
1666
+ description: "List the comments and replies on one Google Doc, as Junior's own Google account sees them. Each comment gives its author, text, the Doc text it is attached to, and whether it is resolved. Works for Docs shared with that account and Docs shared with anyone who has the link. Returns up to 100 comments per call; pass nextPageToken as pageToken to read more. Comments can be private: share only what the requester needs.",
1667
+ inputSchema: inputSchema4,
1668
+ outputSchema: outputSchema4,
1669
+ async execute(input) {
1670
+ const doc = parseDocReference(input.document);
1671
+ await readDocFile(ctx, doc);
1672
+ const response = await driveFileRequest(ctx, doc, {
1673
+ operation: "google.drive.comments.list",
1674
+ path: `/drive/v3/files/${doc.fileId}/comments`,
1675
+ query: {
1676
+ fields: COMMENT_FIELDS,
1677
+ pageSize: "100",
1678
+ ...input.pageToken ? { pageToken: input.pageToken } : void 0
1679
+ }
1680
+ });
1681
+ const { comments, nextPageToken } = commentListSchema.parse(
1682
+ response.body
1683
+ );
1684
+ return {
1685
+ target: "listGoogleDocComments",
1686
+ comments: comments.map((comment) => ({
1687
+ ...comment.author?.displayName ? { author: comment.author.displayName } : void 0,
1688
+ commentId: comment.id,
1689
+ content: comment.content,
1690
+ ...comment.createdTime ? { createdTime: comment.createdTime } : void 0,
1691
+ ...comment.quotedFileContent?.value ? { quotedText: comment.quotedFileContent.value } : void 0,
1692
+ replies: comment.replies.map((reply) => ({
1693
+ ...reply.action ? { action: reply.action } : void 0,
1694
+ ...reply.author?.displayName ? { author: reply.author.displayName } : void 0,
1695
+ content: reply.content,
1696
+ ...reply.createdTime ? { createdTime: reply.createdTime } : void 0
1697
+ })),
1698
+ resolved: comment.resolved === true
1699
+ })),
1700
+ documentId: doc.fileId,
1701
+ ...nextPageToken ? { nextPageToken } : void 0
1702
+ };
1703
+ }
1704
+ });
1705
+ }
1706
+
1707
+ // src/tools/list-events.ts
1708
+ import {
1709
+ definePluginTool as definePluginTool5,
1710
+ PluginToolInputError as PluginToolInputError5,
1711
+ pluginToolOutputSchema as pluginToolOutputSchema5
1712
+ } from "@sentry/junior-plugin-api";
1713
+ import { z as z8 } from "zod";
1507
1714
  var MAX_WINDOW_MS2 = 31 * 24 * 60 * 60 * 1e3;
1508
- var inputSchema4 = z6.object({
1509
- calendar: z6.string().trim().toLowerCase().pipe(z6.email()).optional().describe(
1715
+ var inputSchema5 = z8.object({
1716
+ calendar: z8.string().trim().toLowerCase().pipe(z8.email()).optional().describe(
1510
1717
  "Email address of the person whose calendar to read. Omit to read Junior's own calendar, which holds every event Junior organizes."
1511
1718
  ),
1512
- timeMin: z6.iso.datetime({ offset: true }).describe("Start of the window, RFC 3339 with offset."),
1513
- timeMax: z6.iso.datetime({ offset: true }).describe(
1719
+ timeMin: z8.iso.datetime({ offset: true }).describe("Start of the window, RFC 3339 with offset."),
1720
+ timeMax: z8.iso.datetime({ offset: true }).describe(
1514
1721
  "End of the window, RFC 3339 with offset. At most 31 days after timeMin."
1515
1722
  ),
1516
1723
  timeZone: timeZoneSchema,
1517
- maxResults: z6.number().int().min(1).max(100).default(50)
1724
+ maxResults: z8.number().int().min(1).max(100).default(50)
1518
1725
  }).strict();
1519
- var eventSchema = z6.object({
1520
- attendees: z6.array(attendeeOutputSchema).optional(),
1521
- end: z6.string(),
1522
- eventId: z6.string(),
1523
- label: z6.string(),
1524
- location: z6.string().optional(),
1525
- organizer: z6.string().optional(),
1526
- organizedByJunior: z6.boolean().describe(
1726
+ var eventSchema = z8.object({
1727
+ attendees: z8.array(attendeeOutputSchema).optional(),
1728
+ end: z8.string(),
1729
+ eventId: z8.string(),
1730
+ label: z8.string(),
1731
+ location: z8.string().optional(),
1732
+ organizer: z8.string().optional(),
1733
+ organizedByJunior: z8.boolean().describe(
1527
1734
  "True when Junior organizes the event, so Junior can change or cancel it with this eventId."
1528
1735
  ),
1529
- seriesEventId: z6.string().optional().describe(
1736
+ seriesEventId: z8.string().optional().describe(
1530
1737
  "Set when this is one occurrence of a repeating event. It is the id of the whole series."
1531
1738
  ),
1532
- start: z6.string(),
1533
- title: z6.string().optional(),
1534
- url: z6.string().optional(),
1535
- videoCallUrl: z6.string().optional()
1739
+ start: z8.string(),
1740
+ title: z8.string().optional(),
1741
+ url: z8.string().optional(),
1742
+ videoCallUrl: z8.string().optional()
1536
1743
  });
1537
- var outputSchema4 = pluginToolOutputSchema4.extend({
1538
- target: z6.literal("listCalendarEvents"),
1539
- calendar: z6.string(),
1540
- events: z6.array(eventSchema),
1541
- timeZone: z6.string(),
1542
- truncated: z6.boolean(),
1543
- visible: z6.boolean()
1744
+ var outputSchema5 = pluginToolOutputSchema5.extend({
1745
+ target: z8.literal("listCalendarEvents"),
1746
+ calendar: z8.string(),
1747
+ events: z8.array(eventSchema),
1748
+ timeZone: z8.string(),
1749
+ truncated: z8.boolean(),
1750
+ visible: z8.boolean()
1544
1751
  });
1545
- var eventTimeSchema = z6.object({
1546
- date: z6.string().optional(),
1547
- dateTime: z6.string().optional()
1752
+ var eventTimeSchema = z8.object({
1753
+ date: z8.string().optional(),
1754
+ dateTime: z8.string().optional()
1548
1755
  });
1549
- var listResponseSchema = z6.object({
1550
- items: z6.array(
1551
- z6.object({
1552
- attendees: z6.array(googleAttendeeSchema).optional(),
1756
+ var listResponseSchema = z8.object({
1757
+ items: z8.array(
1758
+ z8.object({
1759
+ attendees: z8.array(googleAttendeeSchema).optional(),
1553
1760
  end: eventTimeSchema,
1554
- hangoutLink: z6.string().optional(),
1555
- htmlLink: z6.string().optional(),
1556
- id: z6.string(),
1557
- location: z6.string().optional(),
1558
- organizer: z6.object({ email: z6.string().optional() }).optional(),
1559
- recurringEventId: z6.string().optional(),
1761
+ hangoutLink: z8.string().optional(),
1762
+ htmlLink: z8.string().optional(),
1763
+ id: z8.string(),
1764
+ location: z8.string().optional(),
1765
+ organizer: z8.object({ email: z8.string().optional() }).optional(),
1766
+ recurringEventId: z8.string().optional(),
1560
1767
  start: eventTimeSchema,
1561
- status: z6.string().optional(),
1562
- summary: z6.string().optional()
1768
+ status: z8.string().optional(),
1769
+ summary: z8.string().optional()
1563
1770
  })
1564
1771
  ).default([]),
1565
- nextPageToken: z6.string().optional()
1772
+ nextPageToken: z8.string().optional()
1566
1773
  });
1567
1774
  function lastAllDayDate(end) {
1568
1775
  if (!end.date) return void 0;
@@ -1583,7 +1790,7 @@ function eventLabel(start, end, timeZone) {
1583
1790
  return last && last !== first ? `${first} to ${last}, all day` : `${first}, all day`;
1584
1791
  }
1585
1792
  function createListCalendarEventsTool(ctx) {
1586
- return definePluginTool4({
1793
+ return definePluginTool5({
1587
1794
  annotations: {
1588
1795
  destructiveHint: false,
1589
1796
  idempotentHint: true,
@@ -1591,17 +1798,17 @@ function createListCalendarEventsTool(ctx) {
1591
1798
  readOnlyHint: true
1592
1799
  },
1593
1800
  description: "List events on a colleague's Google Calendar or on Junior's own calendar, as seen by Junior's own Google account. Shows titles, times, locations, organizer, and attendee responses when the calendar's sharing settings allow it. Use it to find an event to change or cancel, check who accepted, or see what someone has coming up. Events without a title are busy blocks the owner did not share. Only calendars in the company's Google Workspace domains can be read. Calendar details can be private: share only what the requester needs, and avoid repeating them in public channels.",
1594
- inputSchema: inputSchema4,
1595
- outputSchema: outputSchema4,
1801
+ inputSchema: inputSchema5,
1802
+ outputSchema: outputSchema5,
1596
1803
  async execute(input) {
1597
1804
  const timeZone = input.timeZone ?? await ctx.users.resolveTimezone();
1598
1805
  const timeMinMs = Date.parse(input.timeMin);
1599
1806
  const timeMaxMs = Date.parse(input.timeMax);
1600
1807
  if (timeMaxMs <= timeMinMs) {
1601
- throw new PluginToolInputError4("timeMax must be after timeMin.");
1808
+ throw new PluginToolInputError5("timeMax must be after timeMin.");
1602
1809
  }
1603
1810
  if (timeMaxMs - timeMinMs > MAX_WINDOW_MS2) {
1604
- throw new PluginToolInputError4("Read at most 31 days at a time.");
1811
+ throw new PluginToolInputError5("Read at most 31 days at a time.");
1605
1812
  }
1606
1813
  const calendar = input.calendar ?? ctx.accountEmail;
1607
1814
  requireAllowedEmails([calendar], ctx.allowedDomains);
@@ -1652,29 +1859,167 @@ function createListCalendarEventsTool(ctx) {
1652
1859
  });
1653
1860
  }
1654
1861
 
1862
+ // src/tools/read-doc.ts
1863
+ import {
1864
+ definePluginTool as definePluginTool6,
1865
+ pluginToolOutputSchema as pluginToolOutputSchema6
1866
+ } from "@sentry/junior-plugin-api";
1867
+ import { z as z9 } from "zod";
1868
+ var MAX_CONTENT_CHARS = 1e5;
1869
+ var inputSchema6 = z9.object({
1870
+ document: docReferenceSchema
1871
+ }).strict();
1872
+ var outputSchema6 = pluginToolOutputSchema6.extend({
1873
+ target: z9.literal("readGoogleDoc"),
1874
+ content: z9.string().describe("Doc text as Markdown."),
1875
+ documentId: z9.string(),
1876
+ modifiedTime: z9.string().optional(),
1877
+ title: z9.string(),
1878
+ truncated: z9.boolean().describe(
1879
+ `True when the Doc is longer than ${MAX_CONTENT_CHARS} characters and the end is left out.`
1880
+ ),
1881
+ url: z9.string().optional()
1882
+ });
1883
+ function createReadGoogleDocTool(ctx) {
1884
+ return definePluginTool6({
1885
+ annotations: {
1886
+ destructiveHint: false,
1887
+ idempotentHint: true,
1888
+ openWorldHint: true,
1889
+ readOnlyHint: true
1890
+ },
1891
+ description: "Read the text of one Google Doc as Markdown, as Junior's own Google account sees it. Works for Docs shared with that account and Docs shared with anyone who has the link. Comments are not included; use listGoogleDocComments for them. Doc contents can be private: share only what the requester needs, and avoid repeating them in public channels.",
1892
+ inputSchema: inputSchema6,
1893
+ outputSchema: outputSchema6,
1894
+ async execute(input) {
1895
+ const doc = parseDocReference(input.document);
1896
+ const file = await readDocFile(ctx, doc);
1897
+ const response = await driveFileRequest(ctx, doc, {
1898
+ headers: { Accept: "text/markdown" },
1899
+ operation: "google.drive.file.export",
1900
+ path: `/drive/v3/files/${doc.fileId}/export`,
1901
+ query: { mimeType: "text/markdown" }
1902
+ });
1903
+ const content = typeof response.body === "string" ? response.body : "";
1904
+ return {
1905
+ target: "readGoogleDoc",
1906
+ content: content.slice(0, MAX_CONTENT_CHARS),
1907
+ documentId: file.id,
1908
+ ...file.modifiedTime ? { modifiedTime: file.modifiedTime } : void 0,
1909
+ title: file.name,
1910
+ truncated: content.length > MAX_CONTENT_CHARS,
1911
+ ...file.webViewLink ? { url: file.webViewLink } : void 0
1912
+ };
1913
+ }
1914
+ });
1915
+ }
1916
+
1917
+ // src/tools/search-docs.ts
1918
+ import {
1919
+ definePluginTool as definePluginTool7,
1920
+ pluginToolOutputSchema as pluginToolOutputSchema7
1921
+ } from "@sentry/junior-plugin-api";
1922
+ import { z as z10 } from "zod";
1923
+ var inputSchema7 = z10.object({
1924
+ query: z10.string().trim().min(1).max(500).describe("Words to find in the Doc title or text."),
1925
+ maxResults: z10.number().int().min(1).max(50).default(10)
1926
+ }).strict();
1927
+ var outputSchema7 = pluginToolOutputSchema7.extend({
1928
+ target: z10.literal("searchGoogleDocs"),
1929
+ documents: z10.array(
1930
+ z10.object({
1931
+ documentId: z10.string(),
1932
+ modifiedTime: z10.string().optional(),
1933
+ title: z10.string(),
1934
+ url: z10.string().optional()
1935
+ })
1936
+ ),
1937
+ truncated: z10.boolean().describe(
1938
+ "True when more Docs match, or Google did not search every Drive."
1939
+ )
1940
+ });
1941
+ var fileListSchema = z10.object({
1942
+ files: z10.array(
1943
+ z10.object({
1944
+ id: z10.string(),
1945
+ modifiedTime: z10.string().optional(),
1946
+ name: z10.string(),
1947
+ webViewLink: z10.string().optional()
1948
+ })
1949
+ ).default([]),
1950
+ incompleteSearch: z10.boolean().optional(),
1951
+ nextPageToken: z10.string().optional()
1952
+ });
1953
+ function driveQueryString(value) {
1954
+ return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
1955
+ }
1956
+ function createSearchGoogleDocsTool(ctx) {
1957
+ return definePluginTool7({
1958
+ annotations: {
1959
+ destructiveHint: false,
1960
+ idempotentHint: true,
1961
+ openWorldHint: true,
1962
+ readOnlyHint: true
1963
+ },
1964
+ description: "Find Google Docs shared with Junior's own Google account, by words in the title or text. Docs that are only shared by public link are not found; ask for their link and use readGoogleDoc instead. Returns document ids for readGoogleDoc and listGoogleDocComments.",
1965
+ inputSchema: inputSchema7,
1966
+ outputSchema: outputSchema7,
1967
+ async execute(input) {
1968
+ const response = await googleApiRequest(ctx, {
1969
+ operation: "google.drive.files.list",
1970
+ path: "/drive/v3/files",
1971
+ query: {
1972
+ corpora: "allDrives",
1973
+ fields: "files(id,modifiedTime,name,webViewLink),incompleteSearch,nextPageToken",
1974
+ includeItemsFromAllDrives: "true",
1975
+ pageSize: String(input.maxResults),
1976
+ q: `mimeType = '${GOOGLE_DOC_MIME_TYPE}' and trashed = false and fullText contains ${driveQueryString(input.query)}`,
1977
+ supportsAllDrives: "true"
1978
+ }
1979
+ });
1980
+ if (response.status !== 200) {
1981
+ throw googleApiError("google.drive.files.list", response);
1982
+ }
1983
+ const { files, incompleteSearch, nextPageToken } = fileListSchema.parse(
1984
+ response.body
1985
+ );
1986
+ return {
1987
+ target: "searchGoogleDocs",
1988
+ documents: files.map((file) => ({
1989
+ documentId: file.id,
1990
+ ...file.modifiedTime ? { modifiedTime: file.modifiedTime } : void 0,
1991
+ title: file.name,
1992
+ ...file.webViewLink ? { url: file.webViewLink } : void 0
1993
+ })),
1994
+ truncated: nextPageToken !== void 0 || incompleteSearch === true
1995
+ };
1996
+ }
1997
+ });
1998
+ }
1999
+
1655
2000
  // src/tools/update-event.ts
1656
2001
  import {
1657
- definePluginTool as definePluginTool5,
1658
- PluginToolInputError as PluginToolInputError5,
1659
- pluginToolOutputSchema as pluginToolOutputSchema5
2002
+ definePluginTool as definePluginTool8,
2003
+ PluginToolInputError as PluginToolInputError6,
2004
+ pluginToolOutputSchema as pluginToolOutputSchema8
1660
2005
  } from "@sentry/junior-plugin-api";
1661
- import { z as z7 } from "zod";
1662
- var inputSchema5 = z7.object({
2006
+ import { z as z11 } from "zod";
2007
+ var inputSchema8 = z11.object({
1663
2008
  eventId: ownEventIdSchema,
1664
- title: z7.string().trim().min(1).max(200).optional(),
1665
- description: z7.string().max(4e3).optional().describe(
2009
+ title: z11.string().trim().min(1).max(200).optional(),
2010
+ description: z11.string().max(4e3).optional().describe(
1666
2011
  "Replacement description. Do not include private details the attendees should not see."
1667
2012
  ),
1668
- start: z7.iso.datetime({ offset: true }).optional().describe("New start, RFC 3339 with offset. Give end too."),
1669
- end: z7.iso.datetime({ offset: true }).optional().describe("New end, RFC 3339 with offset. Give start too."),
2013
+ start: z11.iso.datetime({ offset: true }).optional().describe("New start, RFC 3339 with offset. Give end too."),
2014
+ end: z11.iso.datetime({ offset: true }).optional().describe("New end, RFC 3339 with offset. Give start too."),
1670
2015
  timeZone: timeZoneSchema,
1671
- location: z7.string().trim().max(500).optional().describe(
2016
+ location: z11.string().trim().max(500).optional().describe(
1672
2017
  "Replacement location, such as a room name or address. Use an empty string to clear it."
1673
2018
  ),
1674
2019
  addAttendees: emailListSchema(50).default([]).describe("Email addresses to invite as required attendees."),
1675
2020
  addOptionalAttendees: emailListSchema(50).default([]).describe("Email addresses to invite as optional attendees."),
1676
2021
  removeAttendees: emailListSchema(50).default([]).describe("Email addresses to remove from the event."),
1677
- addVideoCall: z7.literal(true).optional().describe("Add a Google Meet link when the event has none.")
2022
+ addVideoCall: z11.literal(true).optional().describe("Add a Google Meet link when the event has none.")
1678
2023
  }).strict().refine(
1679
2024
  (input) => input.start === void 0 === (input.end === void 0),
1680
2025
  {
@@ -1684,12 +2029,12 @@ var inputSchema5 = z7.object({
1684
2029
  (input) => input.title !== void 0 || input.description !== void 0 || input.start !== void 0 || input.location !== void 0 || input.addVideoCall !== void 0 || input.addAttendees.length > 0 || input.addOptionalAttendees.length > 0 || input.removeAttendees.length > 0,
1685
2030
  { message: "Give at least one change." }
1686
2031
  );
1687
- var outputSchema5 = pluginToolOutputSchema5.extend({
1688
- target: z7.literal("updateCalendarEvent"),
2032
+ var outputSchema8 = pluginToolOutputSchema8.extend({
2033
+ target: z11.literal("updateCalendarEvent"),
1689
2034
  ...ownEventOutputFields
1690
2035
  });
1691
2036
  function createUpdateCalendarEventTool(ctx) {
1692
- return definePluginTool5({
2037
+ return definePluginTool8({
1693
2038
  annotations: {
1694
2039
  destructiveHint: false,
1695
2040
  idempotentHint: true,
@@ -1710,17 +2055,17 @@ function createUpdateCalendarEventTool(ctx) {
1710
2055
  return `Update Google Calendar event ${input.eventId}: ${changes.join("; ")}. Attendees get an email about the change.`;
1711
2056
  },
1712
2057
  description: "Change a Google Calendar event organized by Junior's own Google account: title, description, time, location, video call, or attendees. Use it to reschedule, add or remove people, or fix details. Google emails the attendees about the change, and existing attendees keep their responses. Only people invited to the event can ask for changes, and only people in the company's Google Workspace domains can be invited. Junior cannot change events other people organize. For a repeating event, an occurrence id changes only that occurrence; its seriesEventId changes every occurrence. Confirm the change with the requester first unless they already stated it exactly.",
1713
- inputSchema: inputSchema5,
1714
- outputSchema: outputSchema5,
2058
+ inputSchema: inputSchema8,
2059
+ outputSchema: outputSchema8,
1715
2060
  async execute(input) {
1716
2061
  const timeZone = input.timeZone ?? await ctx.users.resolveTimezone();
1717
2062
  if (input.start && input.end) {
1718
2063
  const durationMs = Date.parse(input.end) - Date.parse(input.start);
1719
2064
  if (durationMs <= 0) {
1720
- throw new PluginToolInputError5("end must be after start.");
2065
+ throw new PluginToolInputError6("end must be after start.");
1721
2066
  }
1722
2067
  if (durationMs > MAX_EVENT_MS) {
1723
- throw new PluginToolInputError5("Events can be at most 8 hours long.");
2068
+ throw new PluginToolInputError6("Events can be at most 8 hours long.");
1724
2069
  }
1725
2070
  }
1726
2071
  requireAllowedEmails(
@@ -1729,7 +2074,7 @@ function createUpdateCalendarEventTool(ctx) {
1729
2074
  );
1730
2075
  const read = await readOwnEventForRequester(ctx, input.eventId, "change");
1731
2076
  if (read.status === "cancelled") {
1732
- throw new PluginToolInputError5(
2077
+ throw new PluginToolInputError6(
1733
2078
  "This event is cancelled. Create a new event instead."
1734
2079
  );
1735
2080
  }
@@ -1802,7 +2147,7 @@ function googlePlugin() {
1802
2147
  manifest: {
1803
2148
  name: "google",
1804
2149
  displayName: "Google Workspace",
1805
- description: "Google Calendar scheduling as Junior's own Google Workspace account",
2150
+ description: "Google Calendar scheduling and Google Docs reading as Junior's own Google Workspace account",
1806
2151
  domains: [GOOGLE_API_DOMAIN],
1807
2152
  envVars: {
1808
2153
  [GOOGLE_CLIENT_ID_ENV]: {},
@@ -1838,6 +2183,9 @@ function googlePlugin() {
1838
2183
  createCalendarEvent: createCreateCalendarEventTool(toolContext),
1839
2184
  findMeetingTimes: createFindMeetingTimesTool(toolContext),
1840
2185
  listCalendarEvents: createListCalendarEventsTool(toolContext),
2186
+ listGoogleDocComments: createListGoogleDocCommentsTool(toolContext),
2187
+ readGoogleDoc: createReadGoogleDocTool(toolContext),
2188
+ searchGoogleDocs: createSearchGoogleDocsTool(toolContext),
1841
2189
  updateCalendarEvent: createUpdateCalendarEventTool(toolContext)
1842
2190
  };
1843
2191
  }
@@ -0,0 +1,37 @@
1
+ import { z } from "zod";
2
+ import type { GoogleOperation } from "../credentials";
3
+ import { type GoogleToolContext } from "./shared";
4
+ export declare const GOOGLE_DOC_MIME_TYPE = "application/vnd.google-apps.document";
5
+ /** Doc id or link input for the tools that read one Doc. */
6
+ export declare const docReferenceSchema: z.ZodString;
7
+ /** One Drive file, and the resource key that a link-shared file can need. */
8
+ export interface DocReference {
9
+ fileId: string;
10
+ resourceKey?: string;
11
+ }
12
+ /**
13
+ * Return the file id and resource key from a Doc id or link.
14
+ *
15
+ * Links look like `docs.google.com/document/d/<id>/edit`,
16
+ * `drive.google.com/file/d/<id>/view`, or `drive.google.com/open?id=<id>`.
17
+ * Older Docs shared by link also need the link's `resourcekey` value.
18
+ */
19
+ export declare function parseDocReference(value: string): DocReference;
20
+ /** Call one Drive operation on a file, with its resource key when known. */
21
+ export declare function driveFileRequest(ctx: GoogleToolContext, doc: DocReference, input: {
22
+ headers?: Record<string, string>;
23
+ operation: GoogleOperation;
24
+ path: string;
25
+ query?: Record<string, string>;
26
+ }): Promise<{
27
+ body: unknown;
28
+ status: number;
29
+ }>;
30
+ /** Read the Drive metadata of one file, and require it to be a Google Doc. */
31
+ export declare function readDocFile(ctx: GoogleToolContext, doc: DocReference): Promise<{
32
+ id: string;
33
+ mimeType: string;
34
+ name: string;
35
+ modifiedTime?: string | undefined;
36
+ webViewLink?: string | undefined;
37
+ }>;
@@ -0,0 +1,152 @@
1
+ import type { GoogleToolContext } from "./shared";
2
+ /** List the comments and replies on one Google Doc. */
3
+ export declare function createListGoogleDocCommentsTool(ctx: GoogleToolContext): import("@sentry/junior-plugin-api").PluginToolDefinition<{
4
+ document: string;
5
+ pageToken?: string | undefined;
6
+ }, {
7
+ [x: string]: unknown;
8
+ target: "listGoogleDocComments";
9
+ comments: {
10
+ commentId: string;
11
+ content: string;
12
+ replies: {
13
+ content: string;
14
+ action?: "resolve" | "reopen" | undefined;
15
+ author?: string | undefined;
16
+ createdTime?: string | undefined;
17
+ }[];
18
+ resolved: boolean;
19
+ author?: string | undefined;
20
+ createdTime?: string | undefined;
21
+ quotedText?: string | undefined;
22
+ }[];
23
+ documentId: string;
24
+ objectAnnotations?: {
25
+ kind: "object";
26
+ key: string;
27
+ label: string;
28
+ objectType: "code_change" | "task" | "deployment" | "calendar_event" | "automation" | "item";
29
+ title: string;
30
+ url: string | null;
31
+ description?: string | undefined;
32
+ status?: string | undefined;
33
+ trigger?: string | undefined;
34
+ warning?: string | undefined;
35
+ displayType?: string | undefined;
36
+ sourceUpdatedAt?: string | undefined;
37
+ facts?: {
38
+ type: "code_change";
39
+ author?: string | undefined;
40
+ sourceBranch?: string | undefined;
41
+ targetBranch?: string | undefined;
42
+ reviewers?: string[] | undefined;
43
+ review?: "required" | "approved" | "changes_requested" | undefined;
44
+ checks?: {
45
+ passed: number;
46
+ failed: number;
47
+ pending: number;
48
+ } | undefined;
49
+ mergeable?: boolean | undefined;
50
+ changedFiles?: number | undefined;
51
+ additions?: number | undefined;
52
+ deletions?: number | undefined;
53
+ } | {
54
+ type: "task";
55
+ assignees?: string[] | undefined;
56
+ priority?: string | undefined;
57
+ project?: string | undefined;
58
+ cycle?: string | undefined;
59
+ dueDate?: string | undefined;
60
+ labels?: string[] | undefined;
61
+ } | {
62
+ type: "deployment";
63
+ project?: string | undefined;
64
+ environment?: string | undefined;
65
+ revision?: string | undefined;
66
+ branch?: string | undefined;
67
+ } | {
68
+ type: "calendar_event";
69
+ when?: string | undefined;
70
+ attendees?: string[] | undefined;
71
+ } | undefined;
72
+ }[] | undefined;
73
+ truncated?: boolean | undefined;
74
+ continuation?: {
75
+ arguments: Record<string, unknown>;
76
+ reason?: string | undefined;
77
+ } | undefined;
78
+ nextPageToken?: string | undefined;
79
+ }, {
80
+ [x: string]: unknown;
81
+ target: "listGoogleDocComments";
82
+ comments: {
83
+ commentId: string;
84
+ content: string;
85
+ replies: {
86
+ content: string;
87
+ action?: "resolve" | "reopen" | undefined;
88
+ author?: string | undefined;
89
+ createdTime?: string | undefined;
90
+ }[];
91
+ resolved: boolean;
92
+ author?: string | undefined;
93
+ createdTime?: string | undefined;
94
+ quotedText?: string | undefined;
95
+ }[];
96
+ documentId: string;
97
+ objectAnnotations?: {
98
+ kind: "object";
99
+ key: string;
100
+ label: string;
101
+ objectType: "code_change" | "task" | "deployment" | "calendar_event" | "automation" | "item";
102
+ title: string;
103
+ url: string | null;
104
+ description?: string | undefined;
105
+ status?: string | undefined;
106
+ trigger?: string | undefined;
107
+ warning?: string | undefined;
108
+ displayType?: string | undefined;
109
+ sourceUpdatedAt?: string | undefined;
110
+ facts?: {
111
+ type: "code_change";
112
+ author?: string | undefined;
113
+ sourceBranch?: string | undefined;
114
+ targetBranch?: string | undefined;
115
+ reviewers?: string[] | undefined;
116
+ review?: "required" | "approved" | "changes_requested" | undefined;
117
+ checks?: {
118
+ passed: number;
119
+ failed: number;
120
+ pending: number;
121
+ } | undefined;
122
+ mergeable?: boolean | undefined;
123
+ changedFiles?: number | undefined;
124
+ additions?: number | undefined;
125
+ deletions?: number | undefined;
126
+ } | {
127
+ type: "task";
128
+ assignees?: string[] | undefined;
129
+ priority?: string | undefined;
130
+ project?: string | undefined;
131
+ cycle?: string | undefined;
132
+ dueDate?: string | undefined;
133
+ labels?: string[] | undefined;
134
+ } | {
135
+ type: "deployment";
136
+ project?: string | undefined;
137
+ environment?: string | undefined;
138
+ revision?: string | undefined;
139
+ branch?: string | undefined;
140
+ } | {
141
+ type: "calendar_event";
142
+ when?: string | undefined;
143
+ attendees?: string[] | undefined;
144
+ } | undefined;
145
+ }[] | undefined;
146
+ truncated?: boolean | undefined;
147
+ continuation?: {
148
+ arguments: Record<string, unknown>;
149
+ reason?: string | undefined;
150
+ } | undefined;
151
+ nextPageToken?: string | undefined;
152
+ }>;
@@ -0,0 +1,134 @@
1
+ import type { GoogleToolContext } from "./shared";
2
+ /**
3
+ * Read one Google Doc as Markdown.
4
+ *
5
+ * Drive exports the Doc, so this needs no Docs API. Comments are not in the
6
+ * export; `listGoogleDocComments` reads them.
7
+ */
8
+ export declare function createReadGoogleDocTool(ctx: GoogleToolContext): import("@sentry/junior-plugin-api").PluginToolDefinition<{
9
+ document: string;
10
+ }, {
11
+ [x: string]: unknown;
12
+ target: "readGoogleDoc";
13
+ content: string;
14
+ documentId: string;
15
+ title: string;
16
+ truncated: boolean;
17
+ objectAnnotations?: {
18
+ kind: "object";
19
+ key: string;
20
+ label: string;
21
+ objectType: "code_change" | "task" | "deployment" | "calendar_event" | "automation" | "item";
22
+ title: string;
23
+ url: string | null;
24
+ description?: string | undefined;
25
+ status?: string | undefined;
26
+ trigger?: string | undefined;
27
+ warning?: string | undefined;
28
+ displayType?: string | undefined;
29
+ sourceUpdatedAt?: string | undefined;
30
+ facts?: {
31
+ type: "code_change";
32
+ author?: string | undefined;
33
+ sourceBranch?: string | undefined;
34
+ targetBranch?: string | undefined;
35
+ reviewers?: string[] | undefined;
36
+ review?: "required" | "approved" | "changes_requested" | undefined;
37
+ checks?: {
38
+ passed: number;
39
+ failed: number;
40
+ pending: number;
41
+ } | undefined;
42
+ mergeable?: boolean | undefined;
43
+ changedFiles?: number | undefined;
44
+ additions?: number | undefined;
45
+ deletions?: number | undefined;
46
+ } | {
47
+ type: "task";
48
+ assignees?: string[] | undefined;
49
+ priority?: string | undefined;
50
+ project?: string | undefined;
51
+ cycle?: string | undefined;
52
+ dueDate?: string | undefined;
53
+ labels?: string[] | undefined;
54
+ } | {
55
+ type: "deployment";
56
+ project?: string | undefined;
57
+ environment?: string | undefined;
58
+ revision?: string | undefined;
59
+ branch?: string | undefined;
60
+ } | {
61
+ type: "calendar_event";
62
+ when?: string | undefined;
63
+ attendees?: string[] | undefined;
64
+ } | undefined;
65
+ }[] | undefined;
66
+ continuation?: {
67
+ arguments: Record<string, unknown>;
68
+ reason?: string | undefined;
69
+ } | undefined;
70
+ modifiedTime?: string | undefined;
71
+ url?: string | undefined;
72
+ }, {
73
+ [x: string]: unknown;
74
+ target: "readGoogleDoc";
75
+ content: string;
76
+ documentId: string;
77
+ title: string;
78
+ truncated: boolean;
79
+ objectAnnotations?: {
80
+ kind: "object";
81
+ key: string;
82
+ label: string;
83
+ objectType: "code_change" | "task" | "deployment" | "calendar_event" | "automation" | "item";
84
+ title: string;
85
+ url: string | null;
86
+ description?: string | undefined;
87
+ status?: string | undefined;
88
+ trigger?: string | undefined;
89
+ warning?: string | undefined;
90
+ displayType?: string | undefined;
91
+ sourceUpdatedAt?: string | undefined;
92
+ facts?: {
93
+ type: "code_change";
94
+ author?: string | undefined;
95
+ sourceBranch?: string | undefined;
96
+ targetBranch?: string | undefined;
97
+ reviewers?: string[] | undefined;
98
+ review?: "required" | "approved" | "changes_requested" | undefined;
99
+ checks?: {
100
+ passed: number;
101
+ failed: number;
102
+ pending: number;
103
+ } | undefined;
104
+ mergeable?: boolean | undefined;
105
+ changedFiles?: number | undefined;
106
+ additions?: number | undefined;
107
+ deletions?: number | undefined;
108
+ } | {
109
+ type: "task";
110
+ assignees?: string[] | undefined;
111
+ priority?: string | undefined;
112
+ project?: string | undefined;
113
+ cycle?: string | undefined;
114
+ dueDate?: string | undefined;
115
+ labels?: string[] | undefined;
116
+ } | {
117
+ type: "deployment";
118
+ project?: string | undefined;
119
+ environment?: string | undefined;
120
+ revision?: string | undefined;
121
+ branch?: string | undefined;
122
+ } | {
123
+ type: "calendar_event";
124
+ when?: string | undefined;
125
+ attendees?: string[] | undefined;
126
+ } | undefined;
127
+ }[] | undefined;
128
+ continuation?: {
129
+ arguments: Record<string, unknown>;
130
+ reason?: string | undefined;
131
+ } | undefined;
132
+ modifiedTime?: string | undefined;
133
+ url?: string | undefined;
134
+ }>;
@@ -0,0 +1,132 @@
1
+ import { type GoogleToolContext } from "./shared";
2
+ /** Find Google Docs that Junior's account can open, by title or text. */
3
+ export declare function createSearchGoogleDocsTool(ctx: GoogleToolContext): import("@sentry/junior-plugin-api").PluginToolDefinition<{
4
+ query: string;
5
+ maxResults: number;
6
+ }, {
7
+ [x: string]: unknown;
8
+ target: "searchGoogleDocs";
9
+ documents: {
10
+ documentId: string;
11
+ title: string;
12
+ modifiedTime?: string | undefined;
13
+ url?: string | undefined;
14
+ }[];
15
+ truncated: boolean;
16
+ objectAnnotations?: {
17
+ kind: "object";
18
+ key: string;
19
+ label: string;
20
+ objectType: "code_change" | "task" | "deployment" | "calendar_event" | "automation" | "item";
21
+ title: string;
22
+ url: string | null;
23
+ description?: string | undefined;
24
+ status?: string | undefined;
25
+ trigger?: string | undefined;
26
+ warning?: string | undefined;
27
+ displayType?: string | undefined;
28
+ sourceUpdatedAt?: string | undefined;
29
+ facts?: {
30
+ type: "code_change";
31
+ author?: string | undefined;
32
+ sourceBranch?: string | undefined;
33
+ targetBranch?: string | undefined;
34
+ reviewers?: string[] | undefined;
35
+ review?: "required" | "approved" | "changes_requested" | undefined;
36
+ checks?: {
37
+ passed: number;
38
+ failed: number;
39
+ pending: number;
40
+ } | undefined;
41
+ mergeable?: boolean | undefined;
42
+ changedFiles?: number | undefined;
43
+ additions?: number | undefined;
44
+ deletions?: number | undefined;
45
+ } | {
46
+ type: "task";
47
+ assignees?: string[] | undefined;
48
+ priority?: string | undefined;
49
+ project?: string | undefined;
50
+ cycle?: string | undefined;
51
+ dueDate?: string | undefined;
52
+ labels?: string[] | undefined;
53
+ } | {
54
+ type: "deployment";
55
+ project?: string | undefined;
56
+ environment?: string | undefined;
57
+ revision?: string | undefined;
58
+ branch?: string | undefined;
59
+ } | {
60
+ type: "calendar_event";
61
+ when?: string | undefined;
62
+ attendees?: string[] | undefined;
63
+ } | undefined;
64
+ }[] | undefined;
65
+ continuation?: {
66
+ arguments: Record<string, unknown>;
67
+ reason?: string | undefined;
68
+ } | undefined;
69
+ }, {
70
+ [x: string]: unknown;
71
+ target: "searchGoogleDocs";
72
+ documents: {
73
+ documentId: string;
74
+ title: string;
75
+ modifiedTime?: string | undefined;
76
+ url?: string | undefined;
77
+ }[];
78
+ truncated: boolean;
79
+ objectAnnotations?: {
80
+ kind: "object";
81
+ key: string;
82
+ label: string;
83
+ objectType: "code_change" | "task" | "deployment" | "calendar_event" | "automation" | "item";
84
+ title: string;
85
+ url: string | null;
86
+ description?: string | undefined;
87
+ status?: string | undefined;
88
+ trigger?: string | undefined;
89
+ warning?: string | undefined;
90
+ displayType?: string | undefined;
91
+ sourceUpdatedAt?: string | undefined;
92
+ facts?: {
93
+ type: "code_change";
94
+ author?: string | undefined;
95
+ sourceBranch?: string | undefined;
96
+ targetBranch?: string | undefined;
97
+ reviewers?: string[] | undefined;
98
+ review?: "required" | "approved" | "changes_requested" | undefined;
99
+ checks?: {
100
+ passed: number;
101
+ failed: number;
102
+ pending: number;
103
+ } | undefined;
104
+ mergeable?: boolean | undefined;
105
+ changedFiles?: number | undefined;
106
+ additions?: number | undefined;
107
+ deletions?: number | undefined;
108
+ } | {
109
+ type: "task";
110
+ assignees?: string[] | undefined;
111
+ priority?: string | undefined;
112
+ project?: string | undefined;
113
+ cycle?: string | undefined;
114
+ dueDate?: string | undefined;
115
+ labels?: string[] | undefined;
116
+ } | {
117
+ type: "deployment";
118
+ project?: string | undefined;
119
+ environment?: string | undefined;
120
+ revision?: string | undefined;
121
+ branch?: string | undefined;
122
+ } | {
123
+ type: "calendar_event";
124
+ when?: string | undefined;
125
+ attendees?: string[] | undefined;
126
+ } | undefined;
127
+ }[] | undefined;
128
+ continuation?: {
129
+ arguments: Record<string, unknown>;
130
+ reason?: string | undefined;
131
+ } | undefined;
132
+ }>;
@@ -1,7 +1,7 @@
1
1
  import { type PluginEgress, type User } from "@sentry/junior-plugin-api";
2
2
  import { z } from "zod";
3
3
  import { type GoogleOperation } from "../credentials";
4
- /** Runtime capabilities the Calendar tools use. */
4
+ /** Runtime capabilities the Google tools use. */
5
5
  export interface GoogleToolContext {
6
6
  /** The Workspace account Junior acts as, in lowercase. */
7
7
  accountEmail: string;
@@ -27,9 +27,15 @@ export declare function requesterEmail(ctx: GoogleToolContext): Promise<string |
27
27
  * domain. People rarely type their own address.
28
28
  */
29
29
  export declare function withRequester(ctx: GoogleToolContext, emails: string[]): Promise<string[]>;
30
- /** Call one Google Calendar API operation through host-owned egress. */
30
+ /**
31
+ * Call one Google API operation through host-owned egress.
32
+ *
33
+ * A JSON response body is parsed. Other bodies, such as a Drive export, are
34
+ * returned as text.
35
+ */
31
36
  export declare function googleApiRequest(ctx: GoogleToolContext, input: {
32
37
  body?: unknown;
38
+ headers?: Record<string, string>;
33
39
  operation: GoogleOperation;
34
40
  path: string;
35
41
  query?: Record<string, string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior-google",
3
- "version": "0.242.0",
3
+ "version": "0.243.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -25,7 +25,7 @@
25
25
  "commander": "^14.0.3",
26
26
  "drizzle-orm": "^0.45.2",
27
27
  "zod": "^4.5.4",
28
- "@sentry/junior-plugin-api": "0.242.0"
28
+ "@sentry/junior-plugin-api": "0.243.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@oxlint/plugins": "1.79.0",