@commandable/integration-data 0.0.4 → 0.0.6

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.
Files changed (187) hide show
  1. package/dist/credentials-index.d.ts.map +1 -1
  2. package/dist/credentials-index.js +64 -0
  3. package/dist/credentials-index.js.map +1 -1
  4. package/integrations/airtable/__tests__/get_handlers.test.ts +25 -16
  5. package/integrations/github/__tests__/get_handlers.test.ts +202 -7
  6. package/integrations/github/__tests__/write_handlers.test.ts +178 -24
  7. package/integrations/github/handlers/create_commit.js +5 -12
  8. package/integrations/github/handlers/create_pull_request_review.js +10 -0
  9. package/integrations/github/handlers/create_release.js +14 -0
  10. package/integrations/github/handlers/delete_branch.js +8 -0
  11. package/integrations/github/handlers/delete_file.js +9 -0
  12. package/integrations/github/handlers/fork_repo.js +10 -0
  13. package/integrations/github/handlers/get_commit.js +8 -0
  14. package/integrations/github/handlers/get_file_contents.js +21 -0
  15. package/integrations/github/handlers/get_job_logs.js +6 -0
  16. package/integrations/github/handlers/get_latest_release.js +4 -0
  17. package/integrations/github/handlers/get_me.js +4 -0
  18. package/integrations/github/handlers/get_pull_request.js +4 -0
  19. package/integrations/github/handlers/get_pull_request_diff.js +8 -0
  20. package/integrations/github/handlers/get_repo_tree.js +12 -0
  21. package/integrations/github/handlers/get_workflow_run.js +4 -0
  22. package/integrations/github/handlers/list_branches.js +6 -1
  23. package/integrations/github/handlers/list_commits.js +5 -6
  24. package/integrations/github/handlers/list_issue_comments.js +8 -0
  25. package/integrations/github/handlers/list_issues.js +5 -6
  26. package/integrations/github/handlers/list_labels.js +8 -0
  27. package/integrations/github/handlers/list_pull_request_comments.js +8 -0
  28. package/integrations/github/handlers/list_pull_request_files.js +8 -0
  29. package/integrations/github/handlers/list_pull_requests.js +7 -2
  30. package/integrations/github/handlers/list_releases.js +8 -0
  31. package/integrations/github/handlers/list_tags.js +8 -0
  32. package/integrations/github/handlers/list_workflow_runs.js +11 -0
  33. package/integrations/github/handlers/request_pull_request_reviewers.js +10 -0
  34. package/integrations/github/handlers/search_code.js +8 -0
  35. package/integrations/github/handlers/search_issues.js +8 -0
  36. package/integrations/github/handlers/search_pull_requests.js +8 -0
  37. package/integrations/github/handlers/search_repos.js +10 -0
  38. package/integrations/github/handlers/update_pull_request.js +13 -0
  39. package/integrations/github/manifest.json +58 -20
  40. package/integrations/github/schemas/create_pull_request_review.json +17 -0
  41. package/integrations/github/schemas/create_release.json +16 -0
  42. package/integrations/github/schemas/delete_branch.json +10 -0
  43. package/integrations/github/schemas/delete_file.json +13 -0
  44. package/integrations/github/schemas/fork_repo.json +11 -0
  45. package/integrations/github/schemas/get_commit.json +12 -0
  46. package/integrations/github/schemas/get_file_contents.json +11 -0
  47. package/integrations/github/schemas/get_job_logs.json +10 -0
  48. package/integrations/github/schemas/get_pull_request.json +10 -0
  49. package/integrations/github/schemas/get_pull_request_diff.json +10 -0
  50. package/integrations/github/schemas/get_repo_tree.json +12 -0
  51. package/integrations/github/schemas/get_workflow_run.json +10 -0
  52. package/integrations/github/schemas/list_branches.json +12 -0
  53. package/integrations/github/schemas/list_commits.json +5 -3
  54. package/integrations/github/schemas/list_issue_comments.json +12 -0
  55. package/integrations/github/schemas/list_issues.json +4 -2
  56. package/integrations/github/schemas/list_labels.json +11 -0
  57. package/integrations/github/schemas/list_pull_request_comments.json +12 -0
  58. package/integrations/github/schemas/list_pull_request_files.json +12 -0
  59. package/integrations/github/schemas/list_pull_requests.json +7 -1
  60. package/integrations/github/schemas/list_releases.json +11 -0
  61. package/integrations/github/schemas/list_tags.json +11 -0
  62. package/integrations/github/schemas/list_workflow_runs.json +18 -0
  63. package/integrations/github/schemas/request_pull_request_reviewers.json +20 -0
  64. package/integrations/github/schemas/search_code.json +10 -0
  65. package/integrations/github/schemas/search_issues.json +10 -0
  66. package/integrations/github/schemas/search_pull_requests.json +10 -0
  67. package/integrations/github/schemas/search_repos.json +12 -0
  68. package/integrations/github/schemas/update_pull_request.json +15 -0
  69. package/integrations/google-calendar/__tests__/write_and_admin_handlers.test.ts +0 -13
  70. package/integrations/google-calendar/handlers/get_event.js +5 -1
  71. package/integrations/google-calendar/handlers/list_events.js +2 -0
  72. package/integrations/google-calendar/manifest.json +17 -18
  73. package/integrations/google-calendar/prompt.md +68 -0
  74. package/integrations/google-calendar/schemas/id_calendar_event.json +4 -2
  75. package/integrations/google-calendar/schemas/list_events.json +10 -8
  76. package/integrations/google-docs/__tests__/get_handlers.test.ts +5 -20
  77. package/integrations/google-docs/__tests__/write_handlers.test.ts +38 -52
  78. package/integrations/google-docs/handlers/insert_inline_image_after_first_match.js +1 -1
  79. package/integrations/google-docs/handlers/read_document.js +189 -0
  80. package/integrations/google-docs/manifest.json +16 -31
  81. package/integrations/google-docs/prompt.md +49 -0
  82. package/integrations/google-docs/schemas/insert_inline_image_after_first_match.json +0 -1
  83. package/integrations/google-docs/schemas/{get_document_text.json → read_document.json} +5 -2
  84. package/integrations/google-docs/todo.md +18 -0
  85. package/integrations/google-drive/__tests__/handlers.test.ts +145 -0
  86. package/integrations/google-drive/__tests__/usage_parity.test.ts +9 -0
  87. package/integrations/google-drive/handlers/get_file.js +2 -4
  88. package/integrations/google-drive/handlers/get_file_content.js +41 -0
  89. package/integrations/google-drive/handlers/list_files.js +15 -0
  90. package/integrations/google-drive/handlers/search_files.js +20 -0
  91. package/integrations/google-drive/handlers/share_file.js +20 -0
  92. package/integrations/google-drive/manifest.json +37 -10
  93. package/integrations/google-drive/prompt.md +59 -0
  94. package/integrations/google-drive/schemas/get_file.json +2 -2
  95. package/integrations/google-drive/schemas/get_file_content.json +11 -0
  96. package/integrations/google-drive/schemas/list_files.json +12 -0
  97. package/integrations/google-drive/schemas/search_files.json +14 -0
  98. package/integrations/google-drive/schemas/share_file.json +23 -0
  99. package/integrations/google-gmail/__tests__/get_handlers.test.ts +134 -0
  100. package/integrations/google-gmail/__tests__/usage_parity.test.ts +9 -0
  101. package/integrations/google-gmail/__tests__/write_and_admin_handlers.test.ts +211 -0
  102. package/integrations/google-gmail/credentials.json +57 -0
  103. package/integrations/google-gmail/credentials_hint_oauth_token.md +8 -0
  104. package/integrations/google-gmail/credentials_hint_service_account.md +10 -0
  105. package/integrations/google-gmail/handlers/create_draft_email.js +27 -0
  106. package/integrations/google-gmail/handlers/create_label.js +12 -0
  107. package/integrations/google-gmail/handlers/delete_draft.js +13 -0
  108. package/integrations/google-gmail/handlers/delete_label.js +13 -0
  109. package/integrations/google-gmail/handlers/delete_message.js +13 -0
  110. package/integrations/google-gmail/handlers/delete_thread.js +13 -0
  111. package/integrations/google-gmail/handlers/get_draft.js +6 -0
  112. package/integrations/google-gmail/handlers/get_label.js +6 -0
  113. package/integrations/google-gmail/handlers/get_message.js +14 -0
  114. package/integrations/google-gmail/handlers/get_profile.js +5 -0
  115. package/integrations/google-gmail/handlers/get_thread.js +14 -0
  116. package/integrations/google-gmail/handlers/list_drafts.js +15 -0
  117. package/integrations/google-gmail/handlers/list_labels.js +5 -0
  118. package/integrations/google-gmail/handlers/list_messages.js +19 -0
  119. package/integrations/google-gmail/handlers/list_threads.js +19 -0
  120. package/integrations/google-gmail/handlers/modify_message.js +11 -0
  121. package/integrations/google-gmail/handlers/modify_thread.js +11 -0
  122. package/integrations/google-gmail/handlers/read_email.js +56 -0
  123. package/integrations/google-gmail/handlers/send_draft.js +15 -0
  124. package/integrations/google-gmail/handlers/send_email.js +22 -0
  125. package/integrations/google-gmail/handlers/trash_message.js +6 -0
  126. package/integrations/google-gmail/handlers/trash_thread.js +6 -0
  127. package/integrations/google-gmail/handlers/untrash_message.js +6 -0
  128. package/integrations/google-gmail/handlers/untrash_thread.js +6 -0
  129. package/integrations/google-gmail/handlers/update_label.js +15 -0
  130. package/integrations/google-gmail/manifest.json +33 -0
  131. package/integrations/google-gmail/prompt.md +52 -0
  132. package/integrations/google-gmail/schemas/create_draft_email.json +16 -0
  133. package/integrations/google-gmail/schemas/create_label.json +26 -0
  134. package/integrations/google-gmail/schemas/get_message.json +20 -0
  135. package/integrations/{google-docs/schemas/get_document_structured.json → google-gmail/schemas/get_profile.json} +4 -2
  136. package/integrations/google-gmail/schemas/get_thread.json +20 -0
  137. package/integrations/google-gmail/schemas/id_draft.json +16 -0
  138. package/integrations/google-gmail/schemas/id_label.json +16 -0
  139. package/integrations/google-gmail/schemas/id_message.json +16 -0
  140. package/integrations/google-gmail/schemas/id_thread.json +16 -0
  141. package/integrations/google-gmail/schemas/list_drafts.json +30 -0
  142. package/integrations/{google-sheet/schemas/get_developer_metadata.json → google-gmail/schemas/list_labels.json} +4 -3
  143. package/integrations/google-gmail/schemas/list_messages.json +35 -0
  144. package/integrations/google-gmail/schemas/list_threads.json +35 -0
  145. package/integrations/google-gmail/schemas/modify_message.json +24 -0
  146. package/integrations/google-gmail/schemas/modify_thread.json +24 -0
  147. package/integrations/google-gmail/schemas/read_email.json +10 -0
  148. package/integrations/google-gmail/schemas/send_draft.json +29 -0
  149. package/integrations/google-gmail/schemas/send_email.json +17 -0
  150. package/integrations/google-gmail/schemas/update_label.json +33 -0
  151. package/integrations/google-sheet/__tests__/get_handlers.test.ts +7 -52
  152. package/integrations/google-sheet/__tests__/write_handlers.test.ts +1 -20
  153. package/integrations/google-sheet/handlers/get_spreadsheet.js +2 -0
  154. package/integrations/google-sheet/handlers/read_sheet.js +75 -0
  155. package/integrations/google-sheet/manifest.json +13 -62
  156. package/integrations/google-sheet/prompt.md +49 -0
  157. package/integrations/google-sheet/schemas/get_spreadsheet.json +5 -4
  158. package/integrations/google-sheet/schemas/read_sheet.json +21 -0
  159. package/integrations/google-slides/__tests__/get_handlers.test.ts +13 -9
  160. package/integrations/google-slides/__tests__/write_handlers.test.ts +4 -5
  161. package/integrations/google-slides/handlers/read_presentation.js +51 -0
  162. package/integrations/google-slides/manifest.json +13 -13
  163. package/integrations/google-slides/prompt.md +56 -0
  164. package/integrations/new_integration_prompt.md +5 -1
  165. package/package.json +1 -1
  166. package/integrations/google-calendar/handlers/update_event.js +0 -5
  167. package/integrations/google-calendar/schemas/update_event.json +0 -10
  168. package/integrations/google-docs/handlers/get_document.js +0 -12
  169. package/integrations/google-docs/handlers/get_document_structured.js +0 -6
  170. package/integrations/google-docs/handlers/get_document_text.js +0 -17
  171. package/integrations/google-docs/schemas/get_document.json +0 -11
  172. package/integrations/google-sheet/handlers/batch_clear_values_by_data_filter.js +0 -6
  173. package/integrations/google-sheet/handlers/batch_get_values.js +0 -16
  174. package/integrations/google-sheet/handlers/batch_update_values_by_data_filter.js +0 -16
  175. package/integrations/google-sheet/handlers/get_developer_metadata.js +0 -6
  176. package/integrations/google-sheet/handlers/get_spreadsheet_by_data_filter.js +0 -10
  177. package/integrations/google-sheet/handlers/get_values.js +0 -14
  178. package/integrations/google-sheet/handlers/get_values_by_data_filter.js +0 -14
  179. package/integrations/google-sheet/handlers/search_developer_metadata.js +0 -7
  180. package/integrations/google-sheet/schemas/batch_clear_values_by_data_filter.json +0 -10
  181. package/integrations/google-sheet/schemas/batch_get_values.json +0 -13
  182. package/integrations/google-sheet/schemas/batch_update_values_by_data_filter.json +0 -25
  183. package/integrations/google-sheet/schemas/get_spreadsheet_by_data_filter.json +0 -11
  184. package/integrations/google-sheet/schemas/get_values.json +0 -13
  185. package/integrations/google-sheet/schemas/get_values_by_data_filter.json +0 -17
  186. package/integrations/google-sheet/schemas/search_developer_metadata.json +0 -14
  187. package/integrations/google-slides/handlers/get_presentation.js +0 -6
@@ -14,6 +14,8 @@ async (input) => {
14
14
  params.set('singleEvents', String(input.singleEvents))
15
15
  if (input.orderBy)
16
16
  params.set('orderBy', input.orderBy)
17
+ if (input.fields)
18
+ params.set('fields', input.fields)
17
19
  const qs = params.toString()
18
20
  const path = `/calendars/${encodeURIComponent(input.calendarId)}/events${qs ? `?${qs}` : ''}`
19
21
  const res = await integration.fetch(path)
@@ -2,25 +2,24 @@
2
2
  "name": "google-calendar",
3
3
  "version": "0.1.0",
4
4
  "tools": [
5
- { "name": "list_calendars", "description": "List calendars for the authenticated user.", "inputSchema": "schemas/empty.json", "handler": "handlers/list_calendars.js", "scope": "read" },
6
- { "name": "get_calendar", "description": "Get a calendar by ID.", "inputSchema": "schemas/id_calendar.json", "handler": "handlers/get_calendar.js", "scope": "read" },
7
- { "name": "list_events", "description": "List events in a calendar with optional time range and query.", "inputSchema": "schemas/list_events.json", "handler": "handlers/list_events.js", "scope": "read" },
8
- { "name": "get_event", "description": "Get a specific event by ID from a calendar.", "inputSchema": "schemas/id_calendar_event.json", "handler": "handlers/get_event.js", "scope": "read" },
9
- { "name": "list_colors", "description": "Get the color definitions for calendars and events.", "inputSchema": "schemas/empty.json", "handler": "handlers/list_colors.js", "scope": "read" },
10
- { "name": "freebusy_query", "description": "Query free/busy information for calendars.", "inputSchema": "schemas/freebusy_query.json", "handler": "handlers/freebusy_query.js", "scope": "read" },
11
- { "name": "list_settings", "description": "List user settings.", "inputSchema": "schemas/empty.json", "handler": "handlers/list_settings.js", "scope": "read" },
5
+ { "name": "list_calendars", "description": "List all calendars in the authenticated user's calendar list, including the primary calendar and any subscribed or shared calendars. Returns calendar IDs needed for list_events, create_event, and other calendar-specific tools. The primary calendar has calendarId='primary'.", "inputSchema": "schemas/empty.json", "handler": "handlers/list_calendars.js", "scope": "read" },
6
+ { "name": "get_calendar", "description": "Get details for a specific calendar by ID, including its summary, description, timezone, and access role. Use list_calendars to find calendar IDs.", "inputSchema": "schemas/id_calendar.json", "handler": "handlers/get_calendar.js", "scope": "read" },
7
+ { "name": "list_events", "description": "List events in a calendar with optional time range, text search, and pagination. Use calendarId='primary' for the user's main calendar. Set singleEvents=true and orderBy='startTime' to get recurring events expanded into individual instances in chronological order. Times must be RFC3339 format (e.g. '2024-01-15T09:00:00Z' or '2024-01-15T09:00:00-05:00'). Defaults to 25 results. Use pageToken from the response for the next page.", "inputSchema": "schemas/list_events.json", "handler": "handlers/list_events.js", "scope": "read" },
8
+ { "name": "get_event", "description": "Get a specific event by its ID from a calendar. Returns full event details including summary, start, end, attendees, location, description, recurrence, and status. Use list_events to find event IDs.", "inputSchema": "schemas/id_calendar_event.json", "handler": "handlers/get_event.js", "scope": "read" },
9
+ { "name": "list_colors", "description": "Get the set of color definitions available for calendars and events. Returns colorId values and their hex codes. Use colorId values in create_event or patch_event to color-code events.", "inputSchema": "schemas/empty.json", "handler": "handlers/list_colors.js", "scope": "read" },
10
+ { "name": "freebusy_query", "description": "Query free/busy availability for one or more calendars within a time range. Useful for finding open slots or checking if attendees are available. Provide timeMin, timeMax (RFC3339), and an items array of calendar IDs. Returns busy time blocks for each calendar.", "inputSchema": "schemas/freebusy_query.json", "handler": "handlers/freebusy_query.js", "scope": "read" },
11
+ { "name": "list_settings", "description": "List the authenticated user's Google Calendar settings, such as timezone, date format, and notification preferences.", "inputSchema": "schemas/empty.json", "handler": "handlers/list_settings.js", "scope": "read" },
12
12
 
13
- { "name": "create_event", "description": "Create an event in a calendar.", "inputSchema": "schemas/create_event.json", "handler": "handlers/create_event.js", "scope": "write" },
14
- { "name": "update_event", "description": "Update an event in a calendar (full update).", "inputSchema": "schemas/update_event.json", "handler": "handlers/update_event.js", "scope": "write" },
15
- { "name": "patch_event", "description": "Patch fields on an event in a calendar.", "inputSchema": "schemas/patch_event.json", "handler": "handlers/patch_event.js", "scope": "write" },
16
- { "name": "delete_event", "description": "Delete an event from a calendar.", "inputSchema": "schemas/id_calendar_event.json", "handler": "handlers/delete_event.js", "scope": "write" },
17
- { "name": "move_event", "description": "Move an event to another calendar.", "inputSchema": "schemas/move_event.json", "handler": "handlers/move_event.js", "scope": "write" },
18
- { "name": "quick_add", "description": "Create an event using natural language text.", "inputSchema": "schemas/quick_add.json", "handler": "handlers/quick_add.js", "scope": "write" },
13
+ { "name": "create_event", "description": "Create a new event in a calendar. Required fields: calendarId, summary, start, end. Use {dateTime, timeZone} for timed events (e.g. {\"dateTime\": \"2024-01-15T10:00:00\", \"timeZone\": \"America/New_York\"}) or {date} for all-day events (e.g. {\"date\": \"2024-01-15\"}). Optional fields: description, location, attendees (array of {email}), recurrence (RRULE strings), reminders, colorId, visibility. The calendarId field is extracted automatically; all other fields are sent as the event body.", "inputSchema": "schemas/create_event.json", "handler": "handlers/create_event.js", "scope": "write" },
14
+ { "name": "patch_event", "description": "Partially update an event by providing only the fields to change. All other fields are preserved. Use this as the standard event update method. Provide changes in a 'body' object along with calendarId and eventId.", "inputSchema": "schemas/patch_event.json", "handler": "handlers/patch_event.js", "scope": "write" },
15
+ { "name": "delete_event", "description": "Delete an event from a calendar. This permanently removes the event. For recurring events, this deletes only the specified instance. Use list_events to find event IDs.", "inputSchema": "schemas/id_calendar_event.json", "handler": "handlers/delete_event.js", "scope": "write" },
16
+ { "name": "move_event", "description": "Move an event from one calendar to another. Provide the source calendarId, eventId, and the destination calendarId. Returns the updated event in the destination calendar.", "inputSchema": "schemas/move_event.json", "handler": "handlers/move_event.js", "scope": "write" },
17
+ { "name": "quick_add", "description": "Create an event using a natural language text string. Parses the text to extract event details automatically. Examples: 'Meeting with Bob tomorrow at 3pm for 1 hour', 'Dentist appointment on Friday at 2pm', 'Weekly standup every Monday at 9am'. Requires calendarId (use 'primary') and text.", "inputSchema": "schemas/quick_add.json", "handler": "handlers/quick_add.js", "scope": "write" },
19
18
 
20
- { "name": "list_acl", "description": "List ACL rules for a calendar.", "inputSchema": "schemas/id_calendar.json", "handler": "handlers/list_acl.js", "scope": "admin" },
21
- { "name": "get_acl", "description": "Get a specific ACL rule by ID for a calendar.", "inputSchema": "schemas/get_acl.json", "handler": "handlers/get_acl.js", "scope": "admin" },
22
- { "name": "insert_acl", "description": "Insert a new ACL rule for a calendar.", "inputSchema": "schemas/insert_acl.json", "handler": "handlers/insert_acl.js", "scope": "admin" },
23
- { "name": "update_acl", "description": "Update an ACL rule for a calendar.", "inputSchema": "schemas/update_acl.json", "handler": "handlers/update_acl.js", "scope": "admin" },
24
- { "name": "delete_acl", "description": "Delete an ACL rule from a calendar.", "inputSchema": "schemas/delete_acl.json", "handler": "handlers/delete_acl.js", "scope": "admin" }
19
+ { "name": "list_acl", "description": "List the Access Control List (ACL) rules for a calendar. Returns rules defining who has access and at what permission level (reader, writer, owner). Use get_calendar to find the calendarId.", "inputSchema": "schemas/id_calendar.json", "handler": "handlers/list_acl.js", "scope": "admin" },
20
+ { "name": "get_acl", "description": "Get a specific ACL rule by its rule ID for a calendar. Use list_acl to find rule IDs.", "inputSchema": "schemas/get_acl.json", "handler": "handlers/get_acl.js", "scope": "admin" },
21
+ { "name": "insert_acl", "description": "Add a new ACL rule to grant a user or group access to a calendar. Roles: 'reader' (view), 'writer' (view + edit events), 'owner' (full control). Scope must include type ('user', 'group', 'domain', or 'default') and optionally value (email or domain).", "inputSchema": "schemas/insert_acl.json", "handler": "handlers/insert_acl.js", "scope": "admin" },
22
+ { "name": "update_acl", "description": "Update an existing ACL rule to change a user's or group's permission level on a calendar. Use list_acl to find the rule ID.", "inputSchema": "schemas/update_acl.json", "handler": "handlers/update_acl.js", "scope": "admin" },
23
+ { "name": "delete_acl", "description": "Remove an ACL rule from a calendar, revoking the associated user's or group's access. Use list_acl to find the rule ID.", "inputSchema": "schemas/delete_acl.json", "handler": "handlers/delete_acl.js", "scope": "admin" }
25
24
  ]
26
25
  }
@@ -0,0 +1,68 @@
1
+ ## Calendar IDs
2
+
3
+ - Use `calendarId='primary'` for the authenticated user's main calendar
4
+ - Use `list_calendars` to discover other calendar IDs (work, shared, subscribed calendars)
5
+ - Calendar IDs typically look like email addresses (e.g. `user@example.com`) or opaque strings for subscribed calendars
6
+
7
+ ## Date and time format
8
+
9
+ All times must be in RFC3339 format:
10
+ - Timed events: `'2024-01-15T10:00:00-05:00'` (with timezone offset) or `'2024-01-15T15:00:00Z'` (UTC)
11
+ - All-day events use date-only format: `'2024-01-15'`
12
+
13
+ ## Creating events
14
+
15
+ For `create_event`, required fields are `calendarId`, `summary`, `start`, and `end`:
16
+
17
+ **Timed event:**
18
+ ```json
19
+ {
20
+ "calendarId": "primary",
21
+ "summary": "Team Meeting",
22
+ "start": { "dateTime": "2024-01-15T10:00:00", "timeZone": "America/New_York" },
23
+ "end": { "dateTime": "2024-01-15T11:00:00", "timeZone": "America/New_York" }
24
+ }
25
+ ```
26
+
27
+ **All-day event:**
28
+ ```json
29
+ {
30
+ "calendarId": "primary",
31
+ "summary": "Company Holiday",
32
+ "start": { "date": "2024-01-15" },
33
+ "end": { "date": "2024-01-16" }
34
+ }
35
+ ```
36
+
37
+ Note: For all-day events, `end.date` should be the day *after* the last day (exclusive end).
38
+
39
+ ## Listing events in chronological order
40
+
41
+ To list upcoming events in start-time order (e.g. "what's on my calendar this week"):
42
+ - Set `singleEvents=true` to expand recurring events into individual instances
43
+ - Set `orderBy='startTime'` (requires `singleEvents=true`)
44
+ - Set `timeMin` to now (current ISO timestamp) and `timeMax` to the end of the desired range
45
+
46
+ ## Quick add
47
+
48
+ `quick_add` parses natural language:
49
+ - `"Meeting with Bob tomorrow at 3pm for 1 hour"`
50
+ - `"Dentist appointment on Friday at 2pm"`
51
+ - `"Weekly standup every Monday at 9am"`
52
+
53
+ ## Free/busy queries
54
+
55
+ Use `freebusy_query` to check availability before scheduling:
56
+ ```json
57
+ {
58
+ "timeMin": "2024-01-15T00:00:00Z",
59
+ "timeMax": "2024-01-15T23:59:59Z",
60
+ "items": [{ "id": "primary" }, { "id": "colleague@example.com" }]
61
+ }
62
+ ```
63
+
64
+ ## Updating events
65
+
66
+ - Use `update_event` for a full replacement (all fields must be provided)
67
+ - Use `patch_event` for partial updates (only provide the fields you want to change in `body`)
68
+ - `patch_event` is preferred when modifying one or two fields to avoid accidentally clearing others
@@ -1,8 +1,10 @@
1
1
  {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
2
3
  "type": "object",
3
4
  "properties": {
4
- "calendarId": { "type": "string", "description": "Calendar ID (use 'primary' for primary)." },
5
- "eventId": { "type": "string", "description": "Event ID." }
5
+ "calendarId": { "type": "string", "description": "Calendar ID. Use 'primary' for the user's main calendar." },
6
+ "eventId": { "type": "string", "description": "Event ID. Obtained from list_events results." },
7
+ "fields": { "type": "string", "description": "Partial response fields selector to reduce response size. Example: 'id,summary,start,end,attendees'." }
6
8
  },
7
9
  "required": ["calendarId", "eventId"],
8
10
  "additionalProperties": false
@@ -1,14 +1,16 @@
1
1
  {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
2
3
  "type": "object",
3
4
  "properties": {
4
- "calendarId": { "type": "string", "description": "Calendar ID (use 'primary' for primary)." },
5
- "timeMin": { "type": "string", "description": "RFC3339 start time" },
6
- "timeMax": { "type": "string", "description": "RFC3339 end time" },
7
- "q": { "type": "string", "description": "Free-text search query" },
8
- "maxResults": { "type": "integer", "minimum": 1, "maximum": 2500 },
9
- "pageToken": { "type": "string" },
10
- "singleEvents": { "type": "boolean", "description": "Expand recurring events" },
11
- "orderBy": { "type": "string", "enum": ["startTime", "updated"] }
5
+ "calendarId": { "type": "string", "description": "Calendar ID. Use 'primary' for the user's main calendar. Find other calendar IDs via list_calendars." },
6
+ "timeMin": { "type": "string", "description": "Start of the time range (RFC3339 format, e.g. '2024-01-15T00:00:00Z' or '2024-01-15T09:00:00-05:00'). Only events ending after this time are returned." },
7
+ "timeMax": { "type": "string", "description": "End of the time range (RFC3339 format). Only events starting before this time are returned." },
8
+ "q": { "type": "string", "description": "Free-text search query matching event summary, description, location, and attendee details." },
9
+ "maxResults": { "type": "integer", "minimum": 1, "maximum": 2500, "default": 25, "description": "Maximum number of events to return. Defaults to 25. Use pageToken from the response for the next page." },
10
+ "pageToken": { "type": "string", "description": "Page token from a previous list_events response to retrieve the next page of results." },
11
+ "singleEvents": { "type": "boolean", "description": "Expand recurring events into individual instances. Set to true with orderBy='startTime' to get events in chronological order." },
12
+ "orderBy": { "type": "string", "enum": ["startTime", "updated"], "description": "Sort order. 'startTime' requires singleEvents=true. 'updated' sorts by last modification time." },
13
+ "fields": { "type": "string", "description": "Partial response fields selector to reduce response size. Example: 'items(id,summary,start,end,attendees)'. See Calendar API fields reference." }
12
14
  },
13
15
  "required": ["calendarId"],
14
16
  "additionalProperties": false
@@ -16,7 +16,7 @@ interface VariantConfig {
16
16
  const variants: VariantConfig[] = [
17
17
  {
18
18
  key: 'service_account',
19
- credentials: () => ({ serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '' }),
19
+ credentials: () => ({ serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '', subject: env.GOOGLE_IMPERSONATE_SUBJECT || '' }),
20
20
  },
21
21
  {
22
22
  key: 'oauth_token',
@@ -70,28 +70,13 @@ suiteOrSkip('google-docs read handlers (live)', () => {
70
70
  await safeCleanup(async () => drive.write('delete_file')({ fileId: folderId }))
71
71
  }, 60000)
72
72
 
73
- it('get_document returns metadata/content', async () => {
73
+ it('read_document returns markdown content', async () => {
74
74
  if (!documentId)
75
75
  return expect(true).toBe(true)
76
- const handler = docs.read('get_document')
76
+ const handler = docs.read('read_document')
77
77
  const result = await handler({ documentId })
78
- expect(result?.documentId || result?.body?.content || result?.title).toBeTruthy()
79
- }, 30000)
80
-
81
- it('get_document_text returns plain text', async () => {
82
- if (!documentId)
83
- return expect(true).toBe(true)
84
- const handler = docs.read('get_document_text')
85
- const result = await handler({ documentId })
86
- expect(typeof result?.text === 'string').toBe(true)
87
- }, 30000)
88
-
89
- it('get_document_structured returns body JSON', async () => {
90
- if (!documentId)
91
- return expect(true).toBe(true)
92
- const handler = docs.read('get_document_structured')
93
- const result = await handler({ documentId })
94
- expect(result?.body || result?.documentId).toBeTruthy()
78
+ expect(result?.documentId || result?.title).toBeTruthy()
79
+ expect(typeof result?.markdown).toBe('string')
95
80
  }, 30000)
96
81
  })
97
82
  }
@@ -16,7 +16,7 @@ interface VariantConfig {
16
16
  const variants: VariantConfig[] = [
17
17
  {
18
18
  key: 'service_account',
19
- credentials: () => ({ serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '' }),
19
+ credentials: () => ({ serviceAccountJson: env.GOOGLE_SERVICE_ACCOUNT_JSON || '', subject: env.GOOGLE_IMPERSONATE_SUBJECT || '' }),
20
20
  },
21
21
  {
22
22
  key: 'oauth_token',
@@ -49,7 +49,9 @@ suiteOrSkip('google-docs write handlers (live)', () => {
49
49
  variant.key,
50
50
  )
51
51
 
52
- const folder = await drive.write('create_folder')({ name: `CmdTest Docs Write ${Date.now()}` })
52
+ const folder = await drive.write('create_folder')({
53
+ name: `CmdTest Docs Write ${Date.now()}`,
54
+ })
53
55
  ctx.folderId = folder?.id
54
56
  expect(ctx.folderId).toBeTruthy()
55
57
 
@@ -86,9 +88,9 @@ suiteOrSkip('google-docs write handlers (live)', () => {
86
88
  const marker = `CmdTest ${Date.now()}`
87
89
  const res = await append_text({ documentId, text: marker })
88
90
  expect(res?.documentId || Array.isArray(res?.replies)).toBeTruthy()
89
- const get_text = docs.read('get_document_text')
90
- const after = await get_text({ documentId })
91
- expect(String(after?.text || '')).toContain(marker)
91
+ const read_document = docs.read('read_document')
92
+ const after = await read_document({ documentId })
93
+ expect(String(after?.markdown || '')).toContain(marker)
92
94
  }, 60000)
93
95
 
94
96
  it('insert_text_after_first_match inserts text near target', async () => {
@@ -96,19 +98,19 @@ suiteOrSkip('google-docs write handlers (live)', () => {
96
98
  if (!documentId)
97
99
  return expect(true).toBe(true)
98
100
  const insert_text_after_first_match = docs.write('insert_text_after_first_match')
99
- const get_text = docs.read('get_document_text')
101
+ const read_document = docs.read('read_document')
100
102
  const anchor = `ANCHOR_${Date.now()}`
101
103
  const appended = docs.write('append_text')
102
- const before = await get_text({ documentId })
103
- if (!String(before?.text || '').includes(anchor))
104
+ const before = await read_document({ documentId })
105
+ if (!String(before?.markdown || '').includes(anchor))
104
106
  await appended({ documentId, text: `\n${anchor}\n` })
105
107
  const insertSnippet = ` CmdTest ${Date.now()} `
106
108
  const res = await insert_text_after_first_match({ documentId, findText: anchor, insertText: insertSnippet, position: 'after' })
107
109
  expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
108
- const after = await get_text({ documentId })
109
- const text = String(after?.text || '')
110
+ const after = await read_document({ documentId })
111
+ const text = String(after?.markdown || '')
110
112
  expect(text).toContain(anchor)
111
- expect(text).toContain(insertSnippet)
113
+ expect(text).toContain(insertSnippet.trim())
112
114
  }, 60000)
113
115
 
114
116
  it('replace_all_text replaces occurrences', async () => {
@@ -125,11 +127,11 @@ suiteOrSkip('google-docs write handlers (live)', () => {
125
127
  if (!documentId)
126
128
  return expect(true).toBe(true)
127
129
  const style_first_match = docs.write('style_first_match')
128
- const get_struct = docs.read('get_document_structured')
130
+ const read_document = docs.read('read_document')
129
131
  const anchor = `ANCHOR_${Date.now()}`
130
132
  const appended = docs.write('append_text')
131
- const before = await get_struct({ documentId })
132
- if (!JSON.stringify(before?.body || {}).includes(anchor))
133
+ const before = await read_document({ documentId })
134
+ if (!String(before?.markdown || '').includes(anchor))
133
135
  await appended({ documentId, text: `\n${anchor}\n` })
134
136
  const res = await style_first_match({ documentId, findText: anchor, textStyle: { bold: true } })
135
137
  expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
@@ -140,16 +142,16 @@ suiteOrSkip('google-docs write handlers (live)', () => {
140
142
  if (!documentId)
141
143
  return expect(true).toBe(true)
142
144
  const insert_table_after_first_match = docs.write('insert_table_after_first_match')
143
- const get_struct = docs.read('get_document_structured')
145
+ const read_document = docs.read('read_document')
144
146
  const anchor = `ANCHOR_${Date.now()}`
145
147
  const appended = docs.write('append_text')
146
- const before = await get_struct({ documentId })
147
- if (!JSON.stringify(before?.body || {}).includes(anchor))
148
+ const before = await read_document({ documentId })
149
+ if (!String(before?.markdown || '').includes(anchor))
148
150
  await appended({ documentId, text: `\n${anchor}\n` })
149
151
  const res = await insert_table_after_first_match({ documentId, findText: anchor, rows: 1, columns: 1 })
150
152
  expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
151
- const after = await get_struct({ documentId })
152
- const hasTable = (after?.body?.content || []).some((el: any) => Boolean(el.table))
153
+ const after = await read_document({ documentId })
154
+ const hasTable = String(after?.markdown || '').includes('|')
153
155
  expect(hasTable).toBe(true)
154
156
  }, 60000)
155
157
 
@@ -158,31 +160,29 @@ suiteOrSkip('google-docs write handlers (live)', () => {
158
160
  if (!documentId)
159
161
  return expect(true).toBe(true)
160
162
  const insert_page_break_after_first_match = docs.write('insert_page_break_after_first_match')
161
- const get_struct = docs.read('get_document_structured')
163
+ const read_document = docs.read('read_document')
162
164
  const anchor = `ANCHOR_${Date.now()}`
163
165
  const appended = docs.write('append_text')
164
- const before = await get_struct({ documentId })
165
- if (!JSON.stringify(before?.body || {}).includes(anchor))
166
+ const before = await read_document({ documentId })
167
+ if (!String(before?.markdown || '').includes(anchor))
166
168
  await appended({ documentId, text: `\n${anchor}\n` })
167
169
  const res = await insert_page_break_after_first_match({ documentId, findText: anchor })
168
170
  expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
169
- const after = await get_struct({ documentId })
170
- const hasBreak = (after?.body?.content || []).some((el: any) => Boolean(el.sectionBreak))
171
- expect(hasBreak).toBe(true)
172
171
  }, 60000)
173
172
 
174
173
  it('insert_inline_image_after_first_match inserts an image when allowed', async () => {
175
- if (!ctx.documentId || !process.env.GDOCS_TEST_IMAGE_URI)
174
+ if (!ctx.documentId)
176
175
  return expect(true).toBe(true)
177
176
  const documentId = ctx.documentId
177
+ const imageUri = 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png'
178
178
  const insert_inline_image_after_first_match = docs.write('insert_inline_image_after_first_match')
179
179
  const anchor = `ANCHOR_${Date.now()}`
180
180
  const appended = docs.write('append_text')
181
- const get_text = docs.read('get_document_text')
182
- const before = await get_text({ documentId })
183
- if (!String(before?.text || '').includes(anchor))
181
+ const read_document = docs.read('read_document')
182
+ const before = await read_document({ documentId })
183
+ if (!String(before?.markdown || '').includes(anchor))
184
184
  await appended({ documentId, text: `\n${anchor}\n` })
185
- const res = await insert_inline_image_after_first_match({ documentId, findText: anchor, uri: process.env.GDOCS_TEST_IMAGE_URI!, altText: 'CmdTest' })
185
+ const res = await insert_inline_image_after_first_match({ documentId, findText: anchor, uri: imageUri })
186
186
  expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
187
187
  }, 60000)
188
188
 
@@ -191,16 +191,16 @@ suiteOrSkip('google-docs write handlers (live)', () => {
191
191
  if (!documentId)
192
192
  return expect(true).toBe(true)
193
193
  const delete_first_match = docs.write('delete_first_match')
194
- const get_text = docs.read('get_document_text')
194
+ const read_document = docs.read('read_document')
195
195
  const anchor = `ANCHOR_${Date.now()}`
196
196
  const appended = docs.write('append_text')
197
- const before = await get_text({ documentId })
198
- if (!String(before?.text || '').includes(anchor))
197
+ const before = await read_document({ documentId })
198
+ if (!String(before?.markdown || '').includes(anchor))
199
199
  await appended({ documentId, text: `\n${anchor}\n` })
200
200
  const res = await delete_first_match({ documentId, findText: anchor })
201
201
  expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
202
- const after = await get_text({ documentId })
203
- expect(String(after?.text || '')).not.toContain(anchor)
202
+ const after = await read_document({ documentId })
203
+ expect(String(after?.markdown || '')).not.toContain(anchor)
204
204
  }, 60000)
205
205
 
206
206
  it('update_paragraph_style_for_first_match updates paragraph style near target', async () => {
@@ -208,28 +208,14 @@ suiteOrSkip('google-docs write handlers (live)', () => {
208
208
  if (!documentId)
209
209
  return expect(true).toBe(true)
210
210
  const update_paragraph_style_for_first_match = docs.write('update_paragraph_style_for_first_match')
211
- const get_struct = docs.read('get_document_structured')
211
+ const read_document = docs.read('read_document')
212
212
  const anchor = `ANCHOR_${Date.now()}`
213
213
  const appended = docs.write('append_text')
214
- const before = await get_struct({ documentId })
215
- if (!JSON.stringify(before?.body || {}).includes(anchor))
214
+ const before = await read_document({ documentId })
215
+ if (!String(before?.markdown || '').includes(anchor))
216
216
  await appended({ documentId, text: `\n${anchor}\n` })
217
217
  const res = await update_paragraph_style_for_first_match({ documentId, findText: anchor, paragraphStyle: { alignment: 'CENTER' } })
218
218
  expect(res?.applied === true || Array.isArray(res?.replies)).toBeTruthy()
219
- const after = await get_struct({ documentId })
220
- let foundAligned = false
221
- for (const el of (after?.body?.content || [])) {
222
- if (!el.paragraph)
223
- continue
224
- const p = el.paragraph
225
- const text = (p.elements || []).map((e: any) => e?.textRun?.content || '').join('')
226
- if (text.includes(anchor)) {
227
- if (p.paragraphStyle?.alignment === 'CENTER')
228
- foundAligned = true
229
- break
230
- }
231
- }
232
- expect(foundAligned).toBe(true)
233
219
  }, 60000)
234
220
 
235
221
  it('update_document_style updates doc style with no-op', async () => {
@@ -34,7 +34,7 @@ async (input) => {
34
34
  return { ok: true }
35
35
 
36
36
  const requests = []
37
- requests.push({ insertInlineImage: { location: { index: baseIndex }, uri, altTextTitle: altText } })
37
+ requests.push({ insertInlineImage: { location: { index: baseIndex }, uri } })
38
38
  requests.push({ replaceAllText: { containsText: { text: marker, matchCase: true }, replaceText: findText } })
39
39
  const res = await integration.fetch(`/documents/${encodeURIComponent(documentId)}:batchUpdate`, { method: 'POST', body: { requests } })
40
40
  return await res.json()
@@ -0,0 +1,189 @@
1
+ async (input) => {
2
+ const MONO_FONTS = new Set([
3
+ 'Courier',
4
+ 'Courier New',
5
+ 'Consolas',
6
+ 'Menlo',
7
+ 'Monaco',
8
+ 'Roboto Mono',
9
+ 'Source Code Pro',
10
+ ])
11
+
12
+ const HEADING_MAP = {
13
+ TITLE: '#',
14
+ SUBTITLE: '##',
15
+ HEADING_1: '#',
16
+ HEADING_2: '##',
17
+ HEADING_3: '###',
18
+ HEADING_4: '####',
19
+ HEADING_5: '#####',
20
+ HEADING_6: '######',
21
+ }
22
+
23
+ const BULLET_GLYPHS = new Set([
24
+ 'BULLET_DISC_CIRCLE_SQUARE',
25
+ 'BULLET_DIAMONDX_ARROW3D_SQUARE',
26
+ 'BULLET_CHECKBOX',
27
+ 'BULLET_ARROW_DIAMOND_DISC',
28
+ 'BULLET_STAR_CIRCLE_SQUARE',
29
+ ])
30
+
31
+ const LIST_NUMBER_GLYPHS = new Set([
32
+ 'DECIMAL',
33
+ 'ZERO_DECIMAL',
34
+ 'UPPER_ALPHA',
35
+ 'ALPHA',
36
+ 'UPPER_ROMAN',
37
+ 'ROMAN',
38
+ ])
39
+
40
+ const trimEndWhitespace = (value) => (value || '').replace(/[ \t]+$/g, '')
41
+
42
+ const escapeCell = (value) =>
43
+ String(value ?? '')
44
+ .replace(/\|/g, '\\|')
45
+ .replace(/\r?\n/g, '<br>')
46
+
47
+ const extractPlainTextFromParagraph = (paragraph) => {
48
+ let text = ''
49
+ for (const element of paragraph?.elements || []) {
50
+ text += element?.textRun?.content || ''
51
+ }
52
+ return trimEndWhitespace(text)
53
+ }
54
+
55
+ const applyTextStyle = (text, textStyle = {}) => {
56
+ const raw = (text || '').replace(/\n/g, '')
57
+ if (!raw) return ''
58
+
59
+ let out = raw
60
+ if (textStyle.link?.url) out = `[${out}](${textStyle.link.url})`
61
+
62
+ const fontFamily = textStyle.weightedFontFamily?.fontFamily || ''
63
+ const isMono = textStyle.smallCaps || MONO_FONTS.has(fontFamily)
64
+
65
+ if (isMono) out = `\`${out}\``
66
+ if (textStyle.bold) out = `**${out}**`
67
+ if (textStyle.italic) out = `*${out}*`
68
+ if (textStyle.strikethrough) out = `~~${out}~~`
69
+
70
+ return out
71
+ }
72
+
73
+ const paragraphToMarkdown = (paragraph, docLists) => {
74
+ const styleType = paragraph?.paragraphStyle?.namedStyleType
75
+ const headingPrefix = HEADING_MAP[styleType] || ''
76
+
77
+ let line = ''
78
+ for (const element of paragraph?.elements || []) {
79
+ line += applyTextStyle(element?.textRun?.content || '', element?.textRun?.textStyle || {})
80
+ }
81
+ line = trimEndWhitespace(line)
82
+
83
+ if (!line) return ''
84
+
85
+ const bullet = paragraph?.bullet
86
+ if (bullet) {
87
+ const nestingLevel = bullet.nestingLevel || 0
88
+ const listMeta = docLists?.[bullet.listId]
89
+ const nesting = listMeta?.listProperties?.nestingLevels?.[nestingLevel]
90
+ const glyphType = nesting?.glyphType || ''
91
+ const isNumbered = LIST_NUMBER_GLYPHS.has(glyphType) && !BULLET_GLYPHS.has(glyphType)
92
+ const indent = ' '.repeat(Math.max(0, nestingLevel))
93
+ return `${indent}${isNumbered ? '1.' : '-'} ${line}`
94
+ }
95
+
96
+ if (headingPrefix) return `${headingPrefix} ${line}`
97
+ return line
98
+ }
99
+
100
+ const tableToMarkdown = (table, docLists) => {
101
+ const rows = table?.tableRows || []
102
+ if (!rows.length) return ''
103
+
104
+ const normalized = rows.map((row) =>
105
+ (row?.tableCells || []).map((cell) => {
106
+ const parts = []
107
+ for (const c of cell?.content || []) {
108
+ if (c?.paragraph) {
109
+ const p = paragraphToMarkdown(c.paragraph, docLists)
110
+ if (p) parts.push(p)
111
+ }
112
+ }
113
+ return escapeCell(parts.join('<br>'))
114
+ }),
115
+ )
116
+
117
+ const width = Math.max(...normalized.map((r) => r.length), 1)
118
+ const padded = normalized.map((r) => [...r, ...Array(width - r.length).fill('')])
119
+ const header = padded[0] || Array(width).fill('')
120
+ const separator = Array(width).fill('---')
121
+ const body = padded.slice(1)
122
+
123
+ const lines = [
124
+ `| ${header.join(' | ')} |`,
125
+ `| ${separator.join(' | ')} |`,
126
+ ...body.map((r) => `| ${r.join(' | ')} |`),
127
+ ]
128
+ return lines.join('\n')
129
+ }
130
+
131
+ const docToPlainText = (docBodyContent) => {
132
+ const lines = []
133
+ for (const item of docBodyContent || []) {
134
+ if (item?.paragraph) {
135
+ const text = extractPlainTextFromParagraph(item.paragraph)
136
+ if (text) lines.push(text)
137
+ } else if (item?.table) {
138
+ for (const row of item.table.tableRows || []) {
139
+ const cells = (row.tableCells || []).map((cell) => {
140
+ const pieces = []
141
+ for (const contentItem of cell.content || []) {
142
+ if (contentItem?.paragraph) {
143
+ const text = extractPlainTextFromParagraph(contentItem.paragraph)
144
+ if (text) pieces.push(text)
145
+ }
146
+ }
147
+ return pieces.join(' ')
148
+ })
149
+ if (cells.some(Boolean)) lines.push(cells.join(' | '))
150
+ }
151
+ }
152
+ }
153
+ return lines.join('\n\n').trim()
154
+ }
155
+
156
+ const { documentId } = input
157
+ const res = await integration.fetch(`/documents/${encodeURIComponent(documentId)}`)
158
+ const doc = await res.json()
159
+
160
+ const content = doc?.body?.content || []
161
+ const lists = doc?.lists || {}
162
+
163
+ const blocks = []
164
+ for (const item of content) {
165
+ if (item?.paragraph) {
166
+ const line = paragraphToMarkdown(item.paragraph, lists)
167
+ if (line) blocks.push(line)
168
+ } else if (item?.table) {
169
+ const table = tableToMarkdown(item.table, lists)
170
+ if (table) blocks.push(table)
171
+ }
172
+ }
173
+
174
+ const markdown = blocks.join('\n\n').trim()
175
+ if (markdown) {
176
+ return {
177
+ documentId: doc?.documentId || documentId,
178
+ title: doc?.title || '',
179
+ markdown,
180
+ }
181
+ }
182
+
183
+ // Escape hatch: return plain text if markdown conversion produced nothing.
184
+ return {
185
+ documentId: doc?.documentId || documentId,
186
+ title: doc?.title || '',
187
+ markdown: docToPlainText(content),
188
+ }
189
+ }