@sentry/junior-google 0.244.0 → 0.245.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
@@ -434,58 +434,93 @@ import {
434
434
  EgressPolicyDenied
435
435
  } from "@sentry/junior-plugin-api";
436
436
  var GOOGLE_API_DOMAIN = "www.googleapis.com";
437
+ var GOOGLE_SHEETS_API_DOMAIN = "sheets.googleapis.com";
438
+ var GOOGLE_SLIDES_API_DOMAIN = "slides.googleapis.com";
439
+ var GOOGLE_API_DOMAINS = [
440
+ GOOGLE_API_DOMAIN,
441
+ GOOGLE_SHEETS_API_DOMAIN,
442
+ GOOGLE_SLIDES_API_DOMAIN
443
+ ];
437
444
  var GRANT_NAME = "account";
438
445
  var LEASE_SAFETY_MS = 6e4;
439
446
  var GOOGLE_OPERATIONS = {
440
447
  "google.calendar.event.create": {
441
448
  access: "write",
449
+ host: GOOGLE_API_DOMAIN,
442
450
  method: "POST",
443
451
  path: /^\/calendar\/v3\/calendars\/primary\/events$/
444
452
  },
445
453
  "google.calendar.event.delete": {
446
454
  access: "write",
455
+ host: GOOGLE_API_DOMAIN,
447
456
  method: "DELETE",
448
457
  path: /^\/calendar\/v3\/calendars\/primary\/events\/[A-Za-z0-9_]+$/
449
458
  },
450
459
  "google.calendar.event.get": {
451
460
  access: "read",
461
+ host: GOOGLE_API_DOMAIN,
452
462
  method: "GET",
453
463
  path: /^\/calendar\/v3\/calendars\/primary\/events\/[A-Za-z0-9_]+$/
454
464
  },
455
465
  "google.calendar.event.update": {
456
466
  access: "write",
467
+ host: GOOGLE_API_DOMAIN,
457
468
  method: "PATCH",
458
469
  path: /^\/calendar\/v3\/calendars\/primary\/events\/[A-Za-z0-9_]+$/
459
470
  },
460
471
  "google.calendar.events.list": {
461
472
  access: "read",
473
+ host: GOOGLE_API_DOMAIN,
462
474
  method: "GET",
463
475
  path: /^\/calendar\/v3\/calendars\/[^/]+\/events$/
464
476
  },
465
477
  "google.calendar.freebusy.query": {
466
478
  access: "read",
479
+ host: GOOGLE_API_DOMAIN,
467
480
  method: "POST",
468
481
  path: /^\/calendar\/v3\/freeBusy$/
469
482
  },
470
483
  "google.drive.comments.list": {
471
484
  access: "read",
485
+ host: GOOGLE_API_DOMAIN,
472
486
  method: "GET",
473
487
  path: /^\/drive\/v3\/files\/[A-Za-z0-9_-]+\/comments$/
474
488
  },
475
489
  "google.drive.file.export": {
476
490
  access: "read",
491
+ host: GOOGLE_API_DOMAIN,
477
492
  method: "GET",
478
493
  path: /^\/drive\/v3\/files\/[A-Za-z0-9_-]+\/export$/
479
494
  },
480
495
  "google.drive.file.get": {
481
496
  access: "read",
497
+ host: GOOGLE_API_DOMAIN,
482
498
  method: "GET",
483
499
  path: /^\/drive\/v3\/files\/[A-Za-z0-9_-]+$/
484
500
  },
485
501
  "google.drive.files.list": {
486
502
  access: "read",
503
+ host: GOOGLE_API_DOMAIN,
487
504
  method: "GET",
488
505
  path: /^\/drive\/v3\/files$/
506
+ },
507
+ "google.sheets.spreadsheet.get": {
508
+ access: "read",
509
+ host: GOOGLE_SHEETS_API_DOMAIN,
510
+ method: "GET",
511
+ path: /^\/v4\/spreadsheets\/[A-Za-z0-9_-]+$/
512
+ },
513
+ "google.sheets.values.batchGet": {
514
+ access: "read",
515
+ host: GOOGLE_SHEETS_API_DOMAIN,
516
+ method: "GET",
517
+ path: /^\/v4\/spreadsheets\/[A-Za-z0-9_-]+\/values:batchGet$/
518
+ },
519
+ "google.slides.presentation.get": {
520
+ access: "read",
521
+ host: GOOGLE_SLIDES_API_DOMAIN,
522
+ method: "GET",
523
+ path: /^\/v1\/presentations\/[A-Za-z0-9_-]+$/
489
524
  }
490
525
  };
491
526
  function googleGrantForEgress(ctx) {
@@ -497,7 +532,7 @@ function googleGrantForEgress(ctx) {
497
532
  );
498
533
  }
499
534
  const url = new URL(ctx.request.url);
500
- if (url.hostname !== GOOGLE_API_DOMAIN || ctx.request.method.toUpperCase() !== rule.method || !rule.path.test(url.pathname)) {
535
+ if (url.hostname !== rule.host || ctx.request.method.toUpperCase() !== rule.method || !rule.path.test(url.pathname)) {
501
536
  throw new EgressPolicyDenied(
502
537
  `Google request does not match operation ${operation}.`
503
538
  );
@@ -547,12 +582,10 @@ async function issueGoogleCredential(ctx) {
547
582
  lease: {
548
583
  account: { id: config.accountEmail, label: config.accountEmail },
549
584
  expiresAt: new Date(token.expiresAtMs - LEASE_SAFETY_MS).toISOString(),
550
- headerTransforms: [
551
- {
552
- domain: GOOGLE_API_DOMAIN,
553
- headers: { Authorization: `Bearer ${token.accessToken}` }
554
- }
555
- ]
585
+ headerTransforms: GOOGLE_API_DOMAINS.map((domain) => ({
586
+ domain,
587
+ headers: { Authorization: `Bearer ${token.accessToken}` }
588
+ }))
556
589
  }
557
590
  };
558
591
  }
@@ -803,15 +836,18 @@ var googleErrorSchema = z2.object({
803
836
  }).optional()
804
837
  });
805
838
  async function googleApiRequest(ctx, input) {
806
- const url = new URL(`https://${GOOGLE_API_DOMAIN}${input.path}`);
839
+ const rule = GOOGLE_OPERATIONS[input.operation];
840
+ const url = new URL(`https://${rule.host}${input.path}`);
807
841
  for (const [key, value] of Object.entries(input.query ?? {})) {
808
- url.searchParams.set(key, value);
842
+ for (const item of [value].flat()) {
843
+ url.searchParams.append(key, item);
844
+ }
809
845
  }
810
846
  const response = await ctx.egress.fetch({
811
847
  operation: input.operation,
812
848
  provider: "google",
813
849
  request: new Request(url, {
814
- method: GOOGLE_OPERATIONS[input.operation].method,
850
+ method: rule.method,
815
851
  headers: {
816
852
  Accept: "application/json",
817
853
  "Content-Type": "application/json",
@@ -833,6 +869,11 @@ function googleApiError(operation, response) {
833
869
  `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
870
  );
835
871
  }
872
+ if (response.status === 403 && error?.details?.some((detail2) => detail2.reason === "SERVICE_DISABLED")) {
873
+ return new Error(
874
+ `The Google API that ${operation} needs is not enabled for Junior's Google Cloud project. A Junior admin must enable it.`
875
+ );
876
+ }
836
877
  const detail = error?.message;
837
878
  const message = `Google ${operation} failed with HTTP ${response.status}${detail ? `: ${detail}` : ""}`;
838
879
  return response.status === 400 ? new PluginToolInputError(message) : new Error(message);
@@ -1530,22 +1571,46 @@ function createFindMeetingTimesTool(ctx) {
1530
1571
  });
1531
1572
  }
1532
1573
 
1533
- // src/tools/list-doc-comments.ts
1574
+ // src/tools/list-file-comments.ts
1534
1575
  import {
1535
1576
  definePluginTool as definePluginTool4,
1536
1577
  pluginToolOutputSchema as pluginToolOutputSchema4
1537
1578
  } from "@sentry/junior-plugin-api";
1538
1579
  import { z as z7 } from "zod";
1539
1580
 
1540
- // src/tools/docs.ts
1581
+ // src/tools/files.ts
1541
1582
  import { PluginToolInputError as PluginToolInputError4 } from "@sentry/junior-plugin-api";
1542
1583
  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
- );
1584
+ var FILE_TYPES = {
1585
+ document: {
1586
+ label: "Google Doc",
1587
+ mimeType: "application/vnd.google-apps.document"
1588
+ },
1589
+ presentation: {
1590
+ label: "Google Slides presentation",
1591
+ mimeType: "application/vnd.google-apps.presentation"
1592
+ },
1593
+ spreadsheet: {
1594
+ label: "Google Sheet",
1595
+ mimeType: "application/vnd.google-apps.spreadsheet"
1596
+ }
1597
+ };
1598
+ var GOOGLE_FILE_TYPES = Object.keys(FILE_TYPES);
1599
+ function googleFileMimeType(type) {
1600
+ return FILE_TYPES[type].mimeType;
1601
+ }
1602
+ function googleFileType(mimeType) {
1603
+ return GOOGLE_FILE_TYPES.find(
1604
+ (type) => FILE_TYPES[type].mimeType === mimeType
1605
+ );
1606
+ }
1607
+ function fileReferenceSchema(name) {
1608
+ return z6.string().trim().min(10).max(2048).describe(
1609
+ `${name} link, or a file id from searchGoogleFiles. Keep the full link for files shared by link: it can carry a resource key that Google needs.`
1610
+ );
1611
+ }
1547
1612
  var FILE_ID_PATTERN = /^[A-Za-z0-9_-]{10,200}$/;
1548
- function parseDocReference(value) {
1613
+ function parseFileReference(value) {
1549
1614
  let fileId = value;
1550
1615
  let resourceKey;
1551
1616
  if (/^https?:\/\//.test(value)) {
@@ -1555,24 +1620,24 @@ function parseDocReference(value) {
1555
1620
  }
1556
1621
  if (!FILE_ID_PATTERN.test(fileId)) {
1557
1622
  throw new PluginToolInputError4(
1558
- "Use a Google Docs link or a document id from searchGoogleDocs."
1623
+ "Use a Google Docs, Sheets, or Slides link, or a file id from searchGoogleFiles."
1559
1624
  );
1560
1625
  }
1561
1626
  return { fileId, ...resourceKey ? { resourceKey } : void 0 };
1562
1627
  }
1563
- async function driveFileRequest(ctx, doc, input) {
1628
+ async function googleFileRequest(ctx, file, input) {
1564
1629
  const response = await googleApiRequest(ctx, {
1565
1630
  ...input,
1566
1631
  headers: {
1567
1632
  ...input.headers,
1568
- ...doc.resourceKey ? {
1569
- "X-Goog-Drive-Resource-Keys": `${doc.fileId}/${doc.resourceKey}`
1633
+ ...file.resourceKey ? {
1634
+ "X-Goog-Drive-Resource-Keys": `${file.fileId}/${file.resourceKey}`
1570
1635
  } : void 0
1571
1636
  }
1572
1637
  });
1573
1638
  if (response.status === 404) {
1574
1639
  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.`
1640
+ `Junior's Google account (${ctx.accountEmail}) cannot open this file. Ask the owner to share it with that account, or to share it with anyone who has the link.`
1576
1641
  );
1577
1642
  }
1578
1643
  if (response.status !== 200) {
@@ -1580,34 +1645,43 @@ async function driveFileRequest(ctx, doc, input) {
1580
1645
  }
1581
1646
  return response;
1582
1647
  }
1583
- var docFileSchema = z6.object({
1648
+ var driveFileSchema = z6.object({
1584
1649
  id: z6.string(),
1585
1650
  mimeType: z6.string(),
1586
1651
  modifiedTime: z6.string().optional(),
1587
1652
  name: z6.string(),
1588
1653
  webViewLink: z6.string().optional()
1589
1654
  });
1590
- async function readDocFile(ctx, doc) {
1591
- const response = await driveFileRequest(ctx, doc, {
1655
+ async function readGoogleFile(ctx, file, types) {
1656
+ const response = await googleFileRequest(ctx, file, {
1592
1657
  operation: "google.drive.file.get",
1593
- path: `/drive/v3/files/${doc.fileId}`,
1658
+ path: `/drive/v3/files/${file.fileId}`,
1594
1659
  query: {
1595
1660
  fields: "id,mimeType,modifiedTime,name,webViewLink",
1596
1661
  supportsAllDrives: "true"
1597
1662
  }
1598
1663
  });
1599
- const file = docFileSchema.parse(response.body);
1600
- if (file.mimeType !== GOOGLE_DOC_MIME_TYPE) {
1664
+ const metadata = driveFileSchema.parse(response.body);
1665
+ const type = googleFileType(metadata.mimeType);
1666
+ if (!type || !types.includes(type)) {
1667
+ const expected = types.map((item) => FILE_TYPES[item].label).join(" or ");
1601
1668
  throw new PluginToolInputError4(
1602
- `"${file.name}" is not a Google Doc (${file.mimeType}). Junior can only read Google Docs.`
1669
+ `"${metadata.name}" is not a ${expected} (${metadata.mimeType}).`
1603
1670
  );
1604
1671
  }
1605
- return file;
1672
+ return { ...metadata, type };
1606
1673
  }
1674
+ function fileOutputFields(file) {
1675
+ return {
1676
+ ...file.modifiedTime ? { modifiedTime: file.modifiedTime } : void 0,
1677
+ ...file.webViewLink ? { url: file.webViewLink } : void 0
1678
+ };
1679
+ }
1680
+ var MAX_CONTENT_CHARS = 1e5;
1607
1681
 
1608
- // src/tools/list-doc-comments.ts
1682
+ // src/tools/list-file-comments.ts
1609
1683
  var inputSchema4 = z7.object({
1610
- document: docReferenceSchema,
1684
+ file: fileReferenceSchema("Google Docs, Sheets, or Slides"),
1611
1685
  pageToken: z7.string().min(1).max(2048).optional().describe("nextPageToken from an earlier call, to read more comments.")
1612
1686
  }).strict();
1613
1687
  var replyOutputSchema = z7.object({
@@ -1621,15 +1695,15 @@ var commentOutputSchema = z7.object({
1621
1695
  commentId: z7.string(),
1622
1696
  content: z7.string(),
1623
1697
  createdTime: z7.string().optional(),
1624
- quotedText: z7.string().optional().describe("The Doc text that the comment is attached to."),
1698
+ quotedText: z7.string().optional().describe("The file text that the comment is attached to."),
1625
1699
  replies: z7.array(replyOutputSchema),
1626
1700
  resolved: z7.boolean()
1627
1701
  });
1628
1702
  var outputSchema4 = pluginToolOutputSchema4.extend({
1629
- target: z7.literal("listGoogleDocComments"),
1703
+ target: z7.literal("listGoogleFileComments"),
1630
1704
  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.")
1705
+ fileId: z7.string(),
1706
+ nextPageToken: z7.string().optional().describe("Set when the file has more comments. Pass it as pageToken.")
1633
1707
  });
1634
1708
  var authorSchema = z7.object({ displayName: z7.string().optional() }).optional();
1635
1709
  var commentListSchema = z7.object({
@@ -1655,7 +1729,7 @@ var commentListSchema = z7.object({
1655
1729
  nextPageToken: z7.string().optional()
1656
1730
  });
1657
1731
  var COMMENT_FIELDS = "comments(author/displayName,content,createdTime,id,quotedFileContent/value,resolved,replies(action,author/displayName,content,createdTime)),nextPageToken";
1658
- function createListGoogleDocCommentsTool(ctx) {
1732
+ function createListGoogleFileCommentsTool(ctx) {
1659
1733
  return definePluginTool4({
1660
1734
  annotations: {
1661
1735
  destructiveHint: false,
@@ -1663,15 +1737,15 @@ function createListGoogleDocCommentsTool(ctx) {
1663
1737
  openWorldHint: true,
1664
1738
  readOnlyHint: true
1665
1739
  },
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.",
1740
+ description: "List the comments and replies on one Google Doc, Sheet, or Slides presentation, as Junior's own Google account sees them. Each comment gives its author, text, the file text it is attached to, and whether it is resolved. Works for files shared with that account and files 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
1741
  inputSchema: inputSchema4,
1668
1742
  outputSchema: outputSchema4,
1669
1743
  async execute(input) {
1670
- const doc = parseDocReference(input.document);
1671
- await readDocFile(ctx, doc);
1672
- const response = await driveFileRequest(ctx, doc, {
1744
+ const ref = parseFileReference(input.file);
1745
+ await readGoogleFile(ctx, ref, GOOGLE_FILE_TYPES);
1746
+ const response = await googleFileRequest(ctx, ref, {
1673
1747
  operation: "google.drive.comments.list",
1674
- path: `/drive/v3/files/${doc.fileId}/comments`,
1748
+ path: `/drive/v3/files/${ref.fileId}/comments`,
1675
1749
  query: {
1676
1750
  fields: COMMENT_FIELDS,
1677
1751
  pageSize: "100",
@@ -1682,7 +1756,7 @@ function createListGoogleDocCommentsTool(ctx) {
1682
1756
  response.body
1683
1757
  );
1684
1758
  return {
1685
- target: "listGoogleDocComments",
1759
+ target: "listGoogleFileComments",
1686
1760
  comments: comments.map((comment) => ({
1687
1761
  ...comment.author?.displayName ? { author: comment.author.displayName } : void 0,
1688
1762
  commentId: comment.id,
@@ -1697,7 +1771,7 @@ function createListGoogleDocCommentsTool(ctx) {
1697
1771
  })),
1698
1772
  resolved: comment.resolved === true
1699
1773
  })),
1700
- documentId: doc.fileId,
1774
+ fileId: ref.fileId,
1701
1775
  ...nextPageToken ? { nextPageToken } : void 0
1702
1776
  };
1703
1777
  }
@@ -1865,9 +1939,8 @@ import {
1865
1939
  pluginToolOutputSchema as pluginToolOutputSchema6
1866
1940
  } from "@sentry/junior-plugin-api";
1867
1941
  import { z as z9 } from "zod";
1868
- var MAX_CONTENT_CHARS = 1e5;
1869
1942
  var inputSchema6 = z9.object({
1870
- document: docReferenceSchema
1943
+ document: fileReferenceSchema("Google Docs")
1871
1944
  }).strict();
1872
1945
  var outputSchema6 = pluginToolOutputSchema6.extend({
1873
1946
  target: z9.literal("readGoogleDoc"),
@@ -1888,16 +1961,16 @@ function createReadGoogleDocTool(ctx) {
1888
1961
  openWorldHint: true,
1889
1962
  readOnlyHint: true
1890
1963
  },
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.",
1964
+ 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 listGoogleFileComments for them. Doc contents can be private: share only what the requester needs, and avoid repeating them in public channels.",
1892
1965
  inputSchema: inputSchema6,
1893
1966
  outputSchema: outputSchema6,
1894
1967
  async execute(input) {
1895
- const doc = parseDocReference(input.document);
1896
- const file = await readDocFile(ctx, doc);
1897
- const response = await driveFileRequest(ctx, doc, {
1968
+ const ref = parseFileReference(input.document);
1969
+ const file = await readGoogleFile(ctx, ref, ["document"]);
1970
+ const response = await googleFileRequest(ctx, ref, {
1898
1971
  headers: { Accept: "text/markdown" },
1899
1972
  operation: "google.drive.file.export",
1900
- path: `/drive/v3/files/${doc.fileId}/export`,
1973
+ path: `/drive/v3/files/${ref.fileId}/export`,
1901
1974
  query: { mimeType: "text/markdown" }
1902
1975
  });
1903
1976
  const content = typeof response.body === "string" ? response.body : "";
@@ -1905,55 +1978,70 @@ function createReadGoogleDocTool(ctx) {
1905
1978
  target: "readGoogleDoc",
1906
1979
  content: content.slice(0, MAX_CONTENT_CHARS),
1907
1980
  documentId: file.id,
1908
- ...file.modifiedTime ? { modifiedTime: file.modifiedTime } : void 0,
1981
+ ...fileOutputFields(file),
1909
1982
  title: file.name,
1910
- truncated: content.length > MAX_CONTENT_CHARS,
1911
- ...file.webViewLink ? { url: file.webViewLink } : void 0
1983
+ truncated: content.length > MAX_CONTENT_CHARS
1912
1984
  };
1913
1985
  }
1914
1986
  });
1915
1987
  }
1916
1988
 
1917
- // src/tools/search-docs.ts
1989
+ // src/tools/read-sheet.ts
1918
1990
  import {
1919
1991
  definePluginTool as definePluginTool7,
1920
1992
  pluginToolOutputSchema as pluginToolOutputSchema7
1921
1993
  } from "@sentry/junior-plugin-api";
1922
1994
  import { z as z10 } from "zod";
1923
1995
  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)
1996
+ spreadsheet: fileReferenceSchema("Google Sheets"),
1997
+ range: z10.string().trim().min(1).max(500).optional().describe(
1998
+ "A1 range to read, such as 'Budget' for one tab or 'Budget!A1:F50'. Omit to read every tab."
1999
+ )
1926
2000
  }).strict();
1927
2001
  var outputSchema7 = pluginToolOutputSchema7.extend({
1928
- target: z10.literal("searchGoogleDocs"),
1929
- documents: z10.array(
2002
+ target: z10.literal("readGoogleSheet"),
2003
+ modifiedTime: z10.string().optional(),
2004
+ ranges: z10.array(
1930
2005
  z10.object({
1931
- documentId: z10.string(),
1932
- modifiedTime: z10.string().optional(),
1933
- title: z10.string(),
1934
- url: z10.string().optional()
2006
+ content: z10.string().describe("Cell values as CSV, as the Sheet shows them."),
2007
+ range: z10.string().describe("The A1 range that Google read."),
2008
+ truncated: z10.boolean().describe("True when rows at the end of this range are left out.")
1935
2009
  })
1936
2010
  ),
2011
+ spreadsheetId: z10.string(),
2012
+ tabs: z10.array(z10.string()).describe("Names of every tab in the Sheet."),
2013
+ title: z10.string(),
1937
2014
  truncated: z10.boolean().describe(
1938
- "True when more Docs match, or Google did not search every Drive."
1939
- )
2015
+ `True when the cells are longer than ${MAX_CONTENT_CHARS} characters and some rows are left out. Pass a smaller range to read them.`
2016
+ ),
2017
+ url: z10.string().optional()
1940
2018
  });
1941
- var fileListSchema = z10.object({
1942
- files: z10.array(
2019
+ var spreadsheetSchema = z10.object({
2020
+ sheets: z10.array(
1943
2021
  z10.object({
1944
- id: z10.string(),
1945
- modifiedTime: z10.string().optional(),
1946
- name: z10.string(),
1947
- webViewLink: z10.string().optional()
2022
+ properties: z10.object({
2023
+ sheetType: z10.string().optional(),
2024
+ title: z10.string()
2025
+ })
1948
2026
  })
1949
- ).default([]),
1950
- incompleteSearch: z10.boolean().optional(),
1951
- nextPageToken: z10.string().optional()
2027
+ ).default([])
1952
2028
  });
1953
- function driveQueryString(value) {
1954
- return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
2029
+ var valuesSchema = z10.object({
2030
+ valueRanges: z10.array(
2031
+ z10.object({
2032
+ range: z10.string(),
2033
+ values: z10.array(z10.array(z10.unknown())).default([])
2034
+ })
2035
+ ).default([])
2036
+ });
2037
+ function tabRange(title) {
2038
+ return `'${title.replace(/'/g, "''")}'`;
2039
+ }
2040
+ function csvCell(value) {
2041
+ const text2 = value === null || value === void 0 ? "" : String(value);
2042
+ return /[",\r\n]/.test(text2) ? `"${text2.replace(/"/g, '""')}"` : text2;
1955
2043
  }
1956
- function createSearchGoogleDocsTool(ctx) {
2044
+ function createReadGoogleSheetTool(ctx) {
1957
2045
  return definePluginTool7({
1958
2046
  annotations: {
1959
2047
  destructiveHint: false,
@@ -1961,19 +2049,251 @@ function createSearchGoogleDocsTool(ctx) {
1961
2049
  openWorldHint: true,
1962
2050
  readOnlyHint: true
1963
2051
  },
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.",
2052
+ description: "Read the cells of one Google Sheet as CSV, as Junior's own Google account sees it. Reads every tab, or one A1 range. Values are formatted as the Sheet shows them; formulas are not included. Works for Sheets shared with that account and Sheets shared with anyone who has the link. Comments are not included; use listGoogleFileComments for them. Sheet contents can be private: share only what the requester needs, and avoid repeating them in public channels.",
1965
2053
  inputSchema: inputSchema7,
1966
2054
  outputSchema: outputSchema7,
1967
2055
  async execute(input) {
2056
+ const ref = parseFileReference(input.spreadsheet);
2057
+ const file = await readGoogleFile(ctx, ref, ["spreadsheet"]);
2058
+ const spreadsheet = await googleFileRequest(ctx, ref, {
2059
+ operation: "google.sheets.spreadsheet.get",
2060
+ path: `/v4/spreadsheets/${ref.fileId}`,
2061
+ query: { fields: "sheets.properties(sheetType,title)" }
2062
+ });
2063
+ const tabs = spreadsheetSchema.parse(spreadsheet.body).sheets.map((sheet) => sheet.properties).filter((tab) => (tab.sheetType ?? "GRID") === "GRID").map((tab) => tab.title);
2064
+ const values = await googleFileRequest(ctx, ref, {
2065
+ operation: "google.sheets.values.batchGet",
2066
+ path: `/v4/spreadsheets/${ref.fileId}/values:batchGet`,
2067
+ query: {
2068
+ majorDimension: "ROWS",
2069
+ ranges: input.range ? [input.range] : tabs.map(tabRange),
2070
+ valueRenderOption: "FORMATTED_VALUE"
2071
+ }
2072
+ });
2073
+ const { valueRanges } = valuesSchema.parse(values.body);
2074
+ let remaining = MAX_CONTENT_CHARS;
2075
+ const ranges = valueRanges.map(({ range, values: values2 }) => {
2076
+ const rows = [];
2077
+ for (const row of values2) {
2078
+ const line = row.map(csvCell).join(",");
2079
+ if (line.length + 1 > remaining) break;
2080
+ rows.push(line);
2081
+ remaining -= line.length + 1;
2082
+ }
2083
+ return {
2084
+ content: rows.join("\n"),
2085
+ range,
2086
+ truncated: rows.length < values2.length
2087
+ };
2088
+ });
2089
+ return {
2090
+ target: "readGoogleSheet",
2091
+ ...fileOutputFields(file),
2092
+ ranges,
2093
+ spreadsheetId: file.id,
2094
+ tabs,
2095
+ title: file.name,
2096
+ truncated: ranges.some((range) => range.truncated)
2097
+ };
2098
+ }
2099
+ });
2100
+ }
2101
+
2102
+ // src/tools/read-slides.ts
2103
+ import {
2104
+ definePluginTool as definePluginTool8,
2105
+ pluginToolOutputSchema as pluginToolOutputSchema8
2106
+ } from "@sentry/junior-plugin-api";
2107
+ import { z as z11 } from "zod";
2108
+ var inputSchema8 = z11.object({
2109
+ presentation: fileReferenceSchema("Google Slides")
2110
+ }).strict();
2111
+ var outputSchema8 = pluginToolOutputSchema8.extend({
2112
+ target: z11.literal("readGoogleSlides"),
2113
+ modifiedTime: z11.string().optional(),
2114
+ presentationId: z11.string(),
2115
+ slideCount: z11.number().int(),
2116
+ slides: z11.array(
2117
+ z11.object({
2118
+ notes: z11.string().optional().describe("Speaker notes."),
2119
+ slideNumber: z11.number().int(),
2120
+ text: z11.string().describe("Text in the shapes and tables of the slide.")
2121
+ })
2122
+ ),
2123
+ title: z11.string(),
2124
+ truncated: z11.boolean().describe(
2125
+ `True when the text is longer than ${MAX_CONTENT_CHARS} characters and the last slides are left out.`
2126
+ ),
2127
+ url: z11.string().optional()
2128
+ });
2129
+ var textSchema = z11.object({
2130
+ textElements: z11.array(
2131
+ z11.object({
2132
+ textRun: z11.object({ content: z11.string().optional() }).optional()
2133
+ })
2134
+ ).default([])
2135
+ }).optional();
2136
+ var pageElementSchema = z11.lazy(
2137
+ () => z11.object({
2138
+ elementGroup: z11.object({ children: z11.array(pageElementSchema).optional() }).optional(),
2139
+ objectId: z11.string().optional(),
2140
+ shape: z11.object({ text: textSchema }).optional(),
2141
+ table: z11.object({
2142
+ tableRows: z11.array(
2143
+ z11.object({
2144
+ tableCells: z11.array(z11.object({ text: textSchema })).optional()
2145
+ })
2146
+ ).optional()
2147
+ }).optional()
2148
+ })
2149
+ );
2150
+ var presentationSchema = z11.object({
2151
+ slides: z11.array(
2152
+ z11.object({
2153
+ pageElements: z11.array(pageElementSchema).default([]),
2154
+ slideProperties: z11.object({
2155
+ notesPage: z11.object({
2156
+ notesProperties: z11.object({ speakerNotesObjectId: z11.string().optional() }).optional(),
2157
+ pageElements: z11.array(pageElementSchema).default([])
2158
+ }).optional()
2159
+ }).optional()
2160
+ })
2161
+ ).default([])
2162
+ });
2163
+ function plainText(text2) {
2164
+ return (text2?.textElements ?? []).map((element) => element.textRun?.content ?? "").join("").trim();
2165
+ }
2166
+ function elementText(element) {
2167
+ if (element.shape) {
2168
+ return [plainText(element.shape.text)];
2169
+ }
2170
+ if (element.table) {
2171
+ return (element.table.tableRows ?? []).map(
2172
+ (row) => (row.tableCells ?? []).map((cell) => plainText(cell.text)).join(" | ")
2173
+ );
2174
+ }
2175
+ return (element.elementGroup?.children ?? []).flatMap(elementText);
2176
+ }
2177
+ function createReadGoogleSlidesTool(ctx) {
2178
+ return definePluginTool8({
2179
+ annotations: {
2180
+ destructiveHint: false,
2181
+ idempotentHint: true,
2182
+ openWorldHint: true,
2183
+ readOnlyHint: true
2184
+ },
2185
+ description: "Read the text and speaker notes of each slide in one Google Slides presentation, as Junior's own Google account sees it. Images and charts are not described. Works for presentations shared with that account and presentations shared with anyone who has the link. Comments are not included; use listGoogleFileComments for them. Slide contents can be private: share only what the requester needs, and avoid repeating them in public channels.",
2186
+ inputSchema: inputSchema8,
2187
+ outputSchema: outputSchema8,
2188
+ async execute(input) {
2189
+ const ref = parseFileReference(input.presentation);
2190
+ const file = await readGoogleFile(ctx, ref, ["presentation"]);
2191
+ const response = await googleFileRequest(ctx, ref, {
2192
+ operation: "google.slides.presentation.get",
2193
+ path: `/v1/presentations/${ref.fileId}`,
2194
+ query: {
2195
+ fields: "slides(pageElements,slideProperties/notesPage(notesProperties,pageElements))"
2196
+ }
2197
+ });
2198
+ const presentation = presentationSchema.parse(response.body);
2199
+ let remaining = MAX_CONTENT_CHARS;
2200
+ const slides = [];
2201
+ for (const [index, slide] of presentation.slides.entries()) {
2202
+ const text2 = slide.pageElements.flatMap(elementText).filter(Boolean).join("\n");
2203
+ const notesPage = slide.slideProperties?.notesPage;
2204
+ const notesId = notesPage?.notesProperties?.speakerNotesObjectId;
2205
+ const notesElement = notesId ? notesPage?.pageElements.find(
2206
+ (element) => element.objectId === notesId
2207
+ ) : void 0;
2208
+ const notes = notesElement ? elementText(notesElement).join("\n") : "";
2209
+ const size = text2.length + notes.length;
2210
+ if (size > remaining) break;
2211
+ remaining -= size;
2212
+ slides.push({
2213
+ ...notes ? { notes } : void 0,
2214
+ slideNumber: index + 1,
2215
+ text: text2
2216
+ });
2217
+ }
2218
+ return {
2219
+ target: "readGoogleSlides",
2220
+ ...fileOutputFields(file),
2221
+ presentationId: file.id,
2222
+ slideCount: presentation.slides.length,
2223
+ slides,
2224
+ title: file.name,
2225
+ truncated: slides.length < presentation.slides.length
2226
+ };
2227
+ }
2228
+ });
2229
+ }
2230
+
2231
+ // src/tools/search-files.ts
2232
+ import {
2233
+ definePluginTool as definePluginTool9,
2234
+ pluginToolOutputSchema as pluginToolOutputSchema9
2235
+ } from "@sentry/junior-plugin-api";
2236
+ import { z as z12 } from "zod";
2237
+ var inputSchema9 = z12.object({
2238
+ query: z12.string().trim().min(1).max(500).describe("Words to find in the file title or text."),
2239
+ types: z12.array(z12.enum(GOOGLE_FILE_TYPES)).min(1).optional().describe(
2240
+ "File types to find: document (Docs), spreadsheet (Sheets), presentation (Slides). Omit to find all three."
2241
+ ),
2242
+ maxResults: z12.number().int().min(1).max(50).default(10)
2243
+ }).strict();
2244
+ var outputSchema9 = pluginToolOutputSchema9.extend({
2245
+ target: z12.literal("searchGoogleFiles"),
2246
+ files: z12.array(
2247
+ z12.object({
2248
+ fileId: z12.string(),
2249
+ modifiedTime: z12.string().optional(),
2250
+ title: z12.string(),
2251
+ type: z12.enum(GOOGLE_FILE_TYPES),
2252
+ url: z12.string().optional()
2253
+ })
2254
+ ),
2255
+ truncated: z12.boolean().describe(
2256
+ "True when more files match, or Google did not search every Drive."
2257
+ )
2258
+ });
2259
+ var fileListSchema = z12.object({
2260
+ files: z12.array(
2261
+ z12.object({
2262
+ id: z12.string(),
2263
+ mimeType: z12.string(),
2264
+ modifiedTime: z12.string().optional(),
2265
+ name: z12.string(),
2266
+ webViewLink: z12.string().optional()
2267
+ })
2268
+ ).default([]),
2269
+ incompleteSearch: z12.boolean().optional(),
2270
+ nextPageToken: z12.string().optional()
2271
+ });
2272
+ function driveQueryString(value) {
2273
+ return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
2274
+ }
2275
+ function createSearchGoogleFilesTool(ctx) {
2276
+ return definePluginTool9({
2277
+ annotations: {
2278
+ destructiveHint: false,
2279
+ idempotentHint: true,
2280
+ openWorldHint: true,
2281
+ readOnlyHint: true
2282
+ },
2283
+ description: "Find Google Docs, Sheets, and Slides shared with Junior's own Google account, by words in the title or text. Files that are only shared by public link are not found; ask for their link instead. Returns file ids for readGoogleDoc, readGoogleSheet, readGoogleSlides, and listGoogleFileComments.",
2284
+ inputSchema: inputSchema9,
2285
+ outputSchema: outputSchema9,
2286
+ async execute(input) {
2287
+ const mimeTypes = (input.types ?? GOOGLE_FILE_TYPES).map((type) => `mimeType = '${googleFileMimeType(type)}'`).join(" or ");
1968
2288
  const response = await googleApiRequest(ctx, {
1969
2289
  operation: "google.drive.files.list",
1970
2290
  path: "/drive/v3/files",
1971
2291
  query: {
1972
2292
  corpora: "allDrives",
1973
- fields: "files(id,modifiedTime,name,webViewLink),incompleteSearch,nextPageToken",
2293
+ fields: "files(id,mimeType,modifiedTime,name,webViewLink),incompleteSearch,nextPageToken",
1974
2294
  includeItemsFromAllDrives: "true",
1975
2295
  pageSize: String(input.maxResults),
1976
- q: `mimeType = '${GOOGLE_DOC_MIME_TYPE}' and trashed = false and fullText contains ${driveQueryString(input.query)}`,
2296
+ q: `(${mimeTypes}) and trashed = false and fullText contains ${driveQueryString(input.query)}`,
1977
2297
  supportsAllDrives: "true"
1978
2298
  }
1979
2299
  });
@@ -1984,13 +2304,18 @@ function createSearchGoogleDocsTool(ctx) {
1984
2304
  response.body
1985
2305
  );
1986
2306
  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
- })),
2307
+ target: "searchGoogleFiles",
2308
+ files: files.flatMap((file) => {
2309
+ const type = googleFileType(file.mimeType);
2310
+ return type ? [
2311
+ {
2312
+ fileId: file.id,
2313
+ ...fileOutputFields(file),
2314
+ title: file.name,
2315
+ type
2316
+ }
2317
+ ] : [];
2318
+ }),
1994
2319
  truncated: nextPageToken !== void 0 || incompleteSearch === true
1995
2320
  };
1996
2321
  }
@@ -1999,27 +2324,27 @@ function createSearchGoogleDocsTool(ctx) {
1999
2324
 
2000
2325
  // src/tools/update-event.ts
2001
2326
  import {
2002
- definePluginTool as definePluginTool8,
2327
+ definePluginTool as definePluginTool10,
2003
2328
  PluginToolInputError as PluginToolInputError6,
2004
- pluginToolOutputSchema as pluginToolOutputSchema8
2329
+ pluginToolOutputSchema as pluginToolOutputSchema10
2005
2330
  } from "@sentry/junior-plugin-api";
2006
- import { z as z11 } from "zod";
2007
- var inputSchema8 = z11.object({
2331
+ import { z as z13 } from "zod";
2332
+ var inputSchema10 = z13.object({
2008
2333
  eventId: ownEventIdSchema,
2009
- title: z11.string().trim().min(1).max(200).optional(),
2010
- description: z11.string().max(4e3).optional().describe(
2334
+ title: z13.string().trim().min(1).max(200).optional(),
2335
+ description: z13.string().max(4e3).optional().describe(
2011
2336
  "Replacement description. Do not include private details the attendees should not see."
2012
2337
  ),
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."),
2338
+ start: z13.iso.datetime({ offset: true }).optional().describe("New start, RFC 3339 with offset. Give end too."),
2339
+ end: z13.iso.datetime({ offset: true }).optional().describe("New end, RFC 3339 with offset. Give start too."),
2015
2340
  timeZone: timeZoneSchema,
2016
- location: z11.string().trim().max(500).optional().describe(
2341
+ location: z13.string().trim().max(500).optional().describe(
2017
2342
  "Replacement location, such as a room name or address. Use an empty string to clear it."
2018
2343
  ),
2019
2344
  addAttendees: emailListSchema(50).default([]).describe("Email addresses to invite as required attendees."),
2020
2345
  addOptionalAttendees: emailListSchema(50).default([]).describe("Email addresses to invite as optional attendees."),
2021
2346
  removeAttendees: emailListSchema(50).default([]).describe("Email addresses to remove from the event."),
2022
- addVideoCall: z11.literal(true).optional().describe("Add a Google Meet link when the event has none.")
2347
+ addVideoCall: z13.literal(true).optional().describe("Add a Google Meet link when the event has none.")
2023
2348
  }).strict().refine(
2024
2349
  (input) => input.start === void 0 === (input.end === void 0),
2025
2350
  {
@@ -2029,12 +2354,12 @@ var inputSchema8 = z11.object({
2029
2354
  (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,
2030
2355
  { message: "Give at least one change." }
2031
2356
  );
2032
- var outputSchema8 = pluginToolOutputSchema8.extend({
2033
- target: z11.literal("updateCalendarEvent"),
2357
+ var outputSchema10 = pluginToolOutputSchema10.extend({
2358
+ target: z13.literal("updateCalendarEvent"),
2034
2359
  ...ownEventOutputFields
2035
2360
  });
2036
2361
  function createUpdateCalendarEventTool(ctx) {
2037
- return definePluginTool8({
2362
+ return definePluginTool10({
2038
2363
  annotations: {
2039
2364
  destructiveHint: false,
2040
2365
  idempotentHint: true,
@@ -2055,8 +2380,8 @@ function createUpdateCalendarEventTool(ctx) {
2055
2380
  return `Update Google Calendar event ${input.eventId}: ${changes.join("; ")}. Attendees get an email about the change.`;
2056
2381
  },
2057
2382
  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.",
2058
- inputSchema: inputSchema8,
2059
- outputSchema: outputSchema8,
2383
+ inputSchema: inputSchema10,
2384
+ outputSchema: outputSchema10,
2060
2385
  async execute(input) {
2061
2386
  const timeZone = input.timeZone ?? await ctx.users.resolveTimezone();
2062
2387
  if (input.start && input.end) {
@@ -2147,8 +2472,8 @@ function googlePlugin() {
2147
2472
  manifest: {
2148
2473
  name: "google",
2149
2474
  displayName: "Google Workspace",
2150
- description: "Google Calendar scheduling and Google Docs reading as Junior's own Google Workspace account",
2151
- domains: [GOOGLE_API_DOMAIN],
2475
+ description: "Google Calendar scheduling and Google Docs, Sheets, and Slides reading as Junior's own Google Workspace account",
2476
+ domains: GOOGLE_API_DOMAINS,
2152
2477
  envVars: {
2153
2478
  [GOOGLE_CLIENT_ID_ENV]: {},
2154
2479
  [GOOGLE_CLIENT_SECRET_ENV]: {},
@@ -2183,9 +2508,11 @@ function googlePlugin() {
2183
2508
  createCalendarEvent: createCreateCalendarEventTool(toolContext),
2184
2509
  findMeetingTimes: createFindMeetingTimesTool(toolContext),
2185
2510
  listCalendarEvents: createListCalendarEventsTool(toolContext),
2186
- listGoogleDocComments: createListGoogleDocCommentsTool(toolContext),
2511
+ listGoogleFileComments: createListGoogleFileCommentsTool(toolContext),
2187
2512
  readGoogleDoc: createReadGoogleDocTool(toolContext),
2188
- searchGoogleDocs: createSearchGoogleDocsTool(toolContext),
2513
+ readGoogleSheet: createReadGoogleSheetTool(toolContext),
2514
+ readGoogleSlides: createReadGoogleSlidesTool(toolContext),
2515
+ searchGoogleFiles: createSearchGoogleFilesTool(toolContext),
2189
2516
  updateCalendarEvent: createUpdateCalendarEventTool(toolContext)
2190
2517
  };
2191
2518
  }