@zackbart/connecta 0.34.1 → 0.36.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.
Files changed (441) hide show
  1. package/CHANGELOG.md +418 -0
  2. package/README.md +65 -105
  3. package/SECURITY.md +3 -2
  4. package/bin/connecta.mjs +41 -16
  5. package/bin/version.mjs +1 -1
  6. package/dist/access-tokens.d.ts +2 -1
  7. package/dist/access-tokens.js +66 -30
  8. package/dist/activity.d.ts +1 -1
  9. package/dist/activity.js +2 -0
  10. package/dist/agent-config.d.ts +23 -0
  11. package/dist/agent-config.js +59 -0
  12. package/dist/agent-description.js +16 -0
  13. package/dist/agent-identity.js +4 -4
  14. package/dist/agent-observation.js +39 -0
  15. package/dist/auth/clerk-sdk/client.d.ts +2 -0
  16. package/dist/auth/clerk-sdk/generated.js +554 -563
  17. package/dist/auth/clerk-transport.js +70 -3
  18. package/dist/auth/clerk.d.ts +13 -0
  19. package/dist/auth/clerk.js +653 -246
  20. package/dist/auth/cloudflare-access.js +46 -42
  21. package/dist/auth/downstream-client-metadata.js +2 -2
  22. package/dist/auth/downstream-oauth.js +2044 -1161
  23. package/dist/auth/downstream-revocation.js +88 -88
  24. package/dist/auth/native-call.js +20 -0
  25. package/dist/auth/native.js +184 -0
  26. package/dist/auth/oauth-start-reset.js +11 -5
  27. package/dist/auth/oauth-token-response.js +121 -102
  28. package/dist/auth/oauth-v2-migration.js +97 -78
  29. package/dist/auth/renewable-grant.js +10 -0
  30. package/dist/auth/static-oauth.js +751 -281
  31. package/dist/auth/vercel-agent.js +266 -0
  32. package/dist/auth-admission.js +204 -0
  33. package/dist/auth-elicitation.js +350 -241
  34. package/dist/authorization-handoff.js +164 -90
  35. package/dist/call-admission-context.js +3 -10
  36. package/dist/call-admission.js +2 -0
  37. package/dist/call-classification.js +9 -0
  38. package/dist/call-recovery.js +32 -13
  39. package/dist/canonical-json.js +8 -0
  40. package/dist/catalog-cache.js +369 -213
  41. package/dist/catalog-service.js +659 -530
  42. package/dist/catalog.js +5 -1
  43. package/dist/concurrency.js +0 -25
  44. package/dist/config-defaults.d.ts +1 -0
  45. package/dist/config-defaults.js +1 -0
  46. package/dist/config-schema.d.ts +3 -1
  47. package/dist/config-schema.js +13 -3
  48. package/dist/config.d.ts +40 -6
  49. package/dist/config.js +40 -26
  50. package/dist/connector-caller.d.ts +4 -0
  51. package/dist/connector-caller.js +17 -4
  52. package/dist/connectors/api-connector.d.ts +29 -7
  53. package/dist/connectors/api-connector.js +145 -57
  54. package/dist/connectors/guarded-fetch.d.ts +51 -17
  55. package/dist/connectors/guarded-fetch.js +215 -121
  56. package/dist/connectors/negotiation-cache.js +20 -10
  57. package/dist/connectors/remote-mcp.d.ts +10 -30
  58. package/dist/connectors/remote-mcp.js +955 -563
  59. package/dist/d1.js +10 -243
  60. package/dist/derived-context.js +5 -0
  61. package/dist/describe-config.d.ts +3 -1
  62. package/dist/describe-config.js +2 -0
  63. package/dist/downstream-input.js +347 -260
  64. package/dist/errors.d.ts +87 -6
  65. package/dist/errors.js +114 -4
  66. package/dist/execute.js +87 -50
  67. package/dist/executor-admission.js +3 -0
  68. package/dist/executors/quickjs.js +163 -112
  69. package/dist/guarded-transport.js +50 -0
  70. package/dist/guest-runtime.js +1 -1
  71. package/dist/http/client.js +439 -0
  72. package/dist/http/errors.js +558 -0
  73. package/dist/http/fields.js +49 -0
  74. package/dist/http/hatch-paging.js +318 -0
  75. package/dist/http/hatch.js +1279 -0
  76. package/dist/http/json-schema.js +267 -0
  77. package/dist/http/node-transport.js +241 -0
  78. package/dist/http/oauth.js +162 -0
  79. package/dist/http/paging.js +227 -0
  80. package/dist/http/provider.js +221 -0
  81. package/dist/http/safety.js +82 -0
  82. package/dist/http/services.js +79 -0
  83. package/dist/http/tools.js +379 -0
  84. package/dist/http/vendor-index.js +138 -0
  85. package/dist/http/vendor.js +448 -0
  86. package/dist/http/write-request.js +66 -0
  87. package/dist/http-policy.d.ts +11 -0
  88. package/dist/http-policy.js +45 -0
  89. package/dist/inbound-credential.js +1 -1
  90. package/dist/index.d.ts +2 -2
  91. package/dist/index.js +4 -0
  92. package/dist/invocation-auth.js +30 -19
  93. package/dist/invocation.js +218 -85
  94. package/dist/mcp-result.js +0 -4
  95. package/dist/membership.d.ts +4 -0
  96. package/dist/meta-output.js +1 -1
  97. package/dist/meta-tools.js +531 -406
  98. package/dist/native-recovery.js +52 -0
  99. package/dist/native-tools.js +382 -0
  100. package/dist/node.d.ts +4 -3
  101. package/dist/node.js +45 -8
  102. package/dist/oauth-handoff.js +177 -104
  103. package/dist/oauth-partition.js +6 -20
  104. package/dist/oauth-sealing.js +6 -7
  105. package/dist/operation-vocabulary.js +101 -0
  106. package/dist/operator-record.d.ts +2 -2
  107. package/dist/operator-record.js +17 -4
  108. package/dist/operator-ui/contract.d.ts +8 -3
  109. package/dist/operator-ui/generated.js +4 -4
  110. package/dist/operator-ui/model.d.ts +2 -3
  111. package/dist/program-writes.js +47 -26
  112. package/dist/provider-call-facts.d.ts +115 -0
  113. package/dist/provider-call-facts.js +182 -0
  114. package/dist/provider-call.d.ts +112 -0
  115. package/dist/provider-call.js +271 -0
  116. package/dist/provider.d.ts +65 -31
  117. package/dist/provider.js +116 -39
  118. package/dist/providers/_shared/google/hatch.js +91 -0
  119. package/dist/providers/_shared/google/kit.js +427 -0
  120. package/dist/providers/_shared/google/result-size.js +15 -2
  121. package/dist/providers/_shared/google/service-account.d.ts +2 -1
  122. package/dist/providers/_shared/google/service-account.js +8 -4
  123. package/dist/providers/_shared/google/workspace.d.ts +6 -210
  124. package/dist/providers/_shared/google/workspace.js +6 -425
  125. package/dist/providers/_shared/rest/compact-index.js +86 -0
  126. package/dist/providers/_shared/rest/kit.js +146 -0
  127. package/dist/providers/_shared/rest/operation-index.js +230 -14
  128. package/dist/providers/_shared/rest/response-media.js +11 -0
  129. package/dist/providers/_shared/rest/value-safety.js +353 -195
  130. package/dist/providers/_shared/sensitive-fields.js +27 -0
  131. package/dist/providers/breeze/api.js +584 -0
  132. package/dist/providers/breeze/index.d.ts +1 -3
  133. package/dist/providers/breeze/index.js +67 -1420
  134. package/dist/providers/breeze/openapi.generated.js +60 -0
  135. package/dist/providers/breeze/skill.generated.js +2 -6
  136. package/dist/providers/breeze/tools.js +696 -0
  137. package/dist/providers/ccb/api.js +496 -0
  138. package/dist/providers/ccb/index.d.ts +15 -37
  139. package/dist/providers/ccb/index.js +77 -1702
  140. package/dist/providers/ccb/lib/constants.d.ts +34 -0
  141. package/dist/providers/ccb/lib/constants.js +74 -0
  142. package/dist/providers/ccb/lib/guide.js +9 -0
  143. package/dist/providers/ccb/lib/verdicts.js +387 -0
  144. package/dist/providers/ccb/openapi.generated.js +397 -0
  145. package/dist/providers/ccb/safety.js +295 -0
  146. package/dist/providers/ccb/skill.generated.js +2 -6
  147. package/dist/providers/ccb/tools.js +853 -0
  148. package/dist/providers/cloudflare/api.js +1066 -0
  149. package/dist/providers/cloudflare/index.d.ts +5 -22
  150. package/dist/providers/cloudflare/index.js +79 -197
  151. package/dist/providers/cloudflare/lib/config.d.ts +16 -0
  152. package/dist/providers/cloudflare/lib/config.js +2 -0
  153. package/dist/providers/cloudflare/lib/discovery.js +29 -0
  154. package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
  155. package/dist/providers/cloudflare/lib/hatch.js +103 -0
  156. package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
  157. package/dist/providers/cloudflare/lib/response-media.js +75 -0
  158. package/dist/providers/cloudflare/openapi.generated.js +4 -3
  159. package/dist/providers/cloudflare/{value-safety.js → safety.js} +487 -0
  160. package/dist/providers/cloudflare/skill.generated.js +2 -10
  161. package/dist/providers/cloudflare/tools.js +360 -0
  162. package/dist/providers/docs/api.js +51 -0
  163. package/dist/providers/docs/index.d.ts +1 -12
  164. package/dist/providers/docs/index.js +53 -1165
  165. package/dist/providers/docs/lib/constants.d.ts +18 -0
  166. package/dist/providers/docs/lib/constants.js +18 -0
  167. package/dist/providers/docs/lib/discovery.js +26 -0
  168. package/dist/providers/docs/lib/requests.js +48 -0
  169. package/dist/providers/docs/lib/schemas.js +168 -0
  170. package/dist/providers/docs/openapi.generated.js +16 -0
  171. package/dist/providers/docs/safety.js +54 -0
  172. package/dist/providers/docs/skill.generated.js +2 -6
  173. package/dist/providers/docs/tools.js +801 -0
  174. package/dist/providers/drive/api.js +177 -0
  175. package/dist/providers/drive/index.d.ts +1 -12
  176. package/dist/providers/drive/index.js +38 -1448
  177. package/dist/providers/drive/lib/constants.d.ts +6 -0
  178. package/dist/providers/drive/lib/constants.js +6 -0
  179. package/dist/providers/drive/lib/contracts.js +532 -0
  180. package/dist/providers/drive/lib/hatch.js +197 -0
  181. package/dist/providers/drive/openapi.generated.js +77 -0
  182. package/dist/providers/drive/safety.js +90 -0
  183. package/dist/providers/drive/skill.generated.js +2 -6
  184. package/dist/providers/drive/tools.js +1290 -0
  185. package/dist/providers/forms/api.js +71 -0
  186. package/dist/providers/forms/index.d.ts +1 -12
  187. package/dist/providers/forms/index.js +49 -1145
  188. package/dist/providers/forms/lib/constants.d.ts +14 -0
  189. package/dist/providers/forms/lib/constants.js +19 -0
  190. package/dist/providers/forms/lib/hatch.js +72 -0
  191. package/dist/providers/forms/openapi.generated.js +23 -0
  192. package/dist/providers/forms/safety.js +34 -0
  193. package/dist/providers/forms/skill.generated.js +2 -8
  194. package/dist/providers/forms/tools.js +1023 -0
  195. package/dist/providers/github/api.js +150 -0
  196. package/dist/providers/github/index.d.ts +4 -5
  197. package/dist/providers/github/index.js +30 -87
  198. package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
  199. package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
  200. package/dist/providers/github/lib/discovery.js +132 -0
  201. package/dist/providers/github/lib/operations.js +58 -0
  202. package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
  203. package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
  204. package/dist/providers/github/openapi.generated.js +1216 -0
  205. package/dist/providers/github/safety.js +216 -0
  206. package/dist/providers/github/skill.generated.js +2 -5
  207. package/dist/providers/github/tools.js +506 -0
  208. package/dist/providers/gmail/api.js +114 -0
  209. package/dist/providers/gmail/index.d.ts +1 -12
  210. package/dist/providers/gmail/index.js +73 -1621
  211. package/dist/providers/gmail/lib/constants.d.ts +13 -0
  212. package/dist/providers/gmail/lib/constants.js +16 -0
  213. package/dist/providers/gmail/lib/index.js +173 -0
  214. package/dist/providers/gmail/lib/schemas.js +176 -0
  215. package/dist/providers/gmail/lib/threading.js +120 -0
  216. package/dist/providers/gmail/openapi.generated.js +92 -0
  217. package/dist/providers/gmail/safety.js +21 -0
  218. package/dist/providers/gmail/skill.generated.js +2 -6
  219. package/dist/providers/gmail/tools.js +1435 -0
  220. package/dist/providers/overflow/api.js +1008 -0
  221. package/dist/providers/overflow/index.d.ts +1 -17
  222. package/dist/providers/overflow/index.js +60 -1488
  223. package/dist/providers/overflow/skill.generated.js +2 -5
  224. package/dist/providers/overflow/tools.js +873 -0
  225. package/dist/providers/planning-center/api.js +467 -0
  226. package/dist/providers/planning-center/index.d.ts +4 -35
  227. package/dist/providers/planning-center/index.js +69 -2445
  228. package/dist/providers/planning-center/lib/constants.d.ts +58 -0
  229. package/dist/providers/planning-center/lib/constants.js +83 -0
  230. package/dist/providers/planning-center/lib/discovery.js +40 -0
  231. package/dist/providers/planning-center/lib/guide.js +39 -0
  232. package/dist/providers/planning-center/lib/json-api.js +83 -0
  233. package/dist/providers/planning-center/lib/verdicts.js +1758 -0
  234. package/dist/providers/planning-center/openapi.generated.js +1764 -0
  235. package/dist/providers/planning-center/safety.js +188 -0
  236. package/dist/providers/planning-center/skill.generated.js +2 -27
  237. package/dist/providers/planning-center/tools.js +1010 -0
  238. package/dist/providers/quickbooks/api.js +299 -0
  239. package/dist/providers/quickbooks/index.d.ts +13 -0
  240. package/dist/providers/quickbooks/index.js +93 -0
  241. package/dist/providers/quickbooks/lib/query.js +118 -0
  242. package/dist/providers/quickbooks/lib/writes.js +168 -0
  243. package/dist/providers/quickbooks/safety.js +222 -0
  244. package/dist/providers/quickbooks/skill.generated.js +5 -0
  245. package/dist/providers/quickbooks/tools.js +117 -0
  246. package/dist/providers/sheets/api.js +133 -0
  247. package/dist/providers/sheets/index.d.ts +4 -16
  248. package/dist/providers/sheets/index.js +61 -1172
  249. package/dist/providers/sheets/lib/constants.d.ts +14 -0
  250. package/dist/providers/sheets/lib/constants.js +14 -0
  251. package/dist/providers/sheets/lib/hatch.js +163 -0
  252. package/dist/providers/sheets/lib/requests.js +105 -0
  253. package/dist/providers/sheets/lib/schemas.js +138 -0
  254. package/dist/providers/sheets/openapi.generated.js +30 -0
  255. package/dist/providers/sheets/safety.js +27 -0
  256. package/dist/providers/sheets/skill.generated.js +2 -7
  257. package/dist/providers/sheets/tools.js +733 -0
  258. package/dist/providers/slides/api.js +147 -0
  259. package/dist/providers/slides/index.d.ts +2 -10
  260. package/dist/providers/slides/index.js +48 -2555
  261. package/dist/providers/slides/lib/constants.d.ts +11 -0
  262. package/dist/providers/slides/lib/constants.js +11 -0
  263. package/dist/providers/slides/lib/requests.js +60 -0
  264. package/dist/providers/slides/lib/schemas.js +565 -0
  265. package/dist/providers/slides/openapi.generated.js +18 -0
  266. package/dist/providers/slides/skill.generated.js +2 -6
  267. package/dist/providers/slides/tools.js +1968 -0
  268. package/dist/providers/stripe/api.js +401 -0
  269. package/dist/providers/stripe/index.d.ts +5 -27
  270. package/dist/providers/stripe/index.js +74 -210
  271. package/dist/providers/stripe/lib/config.d.ts +4 -0
  272. package/dist/providers/stripe/lib/config.js +2 -0
  273. package/dist/providers/stripe/openapi.generated.js +4 -3
  274. package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
  275. package/dist/providers/stripe/skill.generated.js +2 -11
  276. package/dist/providers/stripe/tools.js +77 -0
  277. package/dist/providers/tithely/api.js +531 -0
  278. package/dist/providers/tithely/index.d.ts +17 -0
  279. package/dist/providers/tithely/index.js +86 -965
  280. package/dist/providers/tithely/skill.generated.js +2 -5
  281. package/dist/providers/tithely/tools.js +426 -0
  282. package/dist/providers/vercel/api.js +551 -0
  283. package/dist/providers/vercel/index.d.ts +9 -25
  284. package/dist/providers/vercel/index.js +65 -260
  285. package/dist/providers/vercel/lib/config.d.ts +2 -0
  286. package/dist/providers/vercel/lib/config.js +2 -0
  287. package/dist/providers/vercel/lib/index.js +117 -0
  288. package/dist/providers/vercel/lib/logs.js +100 -0
  289. package/dist/providers/vercel/lib/pin.js +270 -0
  290. package/dist/providers/vercel/openapi.generated.js +10 -5
  291. package/dist/providers/vercel/{value-safety.js → safety.js} +247 -38
  292. package/dist/providers/vercel/skill.generated.js +2 -7
  293. package/dist/providers/vercel/tools.js +642 -0
  294. package/dist/redaction-identity.js +47 -0
  295. package/dist/redaction-marker.js +125 -0
  296. package/dist/registry.d.ts +21 -15
  297. package/dist/registry.js +409 -376
  298. package/dist/routes/access-tokens.js +1 -1
  299. package/dist/routes/actor-labels.js +1 -1
  300. package/dist/routes/connect.js +1 -1
  301. package/dist/routes/dispatch.js +103 -0
  302. package/dist/routes/mcp.js +170 -79
  303. package/dist/routes/oauth.js +6 -1
  304. package/dist/routes/operator.js +2 -2
  305. package/dist/routes/shared.d.ts +5 -4
  306. package/dist/routes/shared.js +244 -177
  307. package/dist/routes/skills.js +10 -8
  308. package/dist/routes/ui-config.js +27 -3
  309. package/dist/routes/ui-data.js +21 -20
  310. package/dist/routes/ui.js +1 -1
  311. package/dist/runtime/call-admission.js +0 -26
  312. package/dist/runtime/call-entry.js +30 -0
  313. package/dist/runtime/call.js +552 -0
  314. package/dist/runtime/connector-scope.js +7 -0
  315. package/dist/runtime/credential-probe.js +34 -0
  316. package/dist/runtime/executor.js +40 -0
  317. package/dist/runtime/native-storage.js +63 -0
  318. package/dist/runtime/registry.js +27 -0
  319. package/dist/runtime/request-scope.js +72 -0
  320. package/dist/runtime/run.js +15 -7
  321. package/dist/runtime/shared-read.js +34 -5
  322. package/dist/runtime/storage.js +34 -21
  323. package/dist/sent-secrets.d.ts +194 -37
  324. package/dist/sent-secrets.js +1743 -340
  325. package/dist/server.js +5 -56
  326. package/dist/skills.js +322 -223
  327. package/dist/sqlite.js +15 -14
  328. package/dist/storage/d1-copy.js +255 -0
  329. package/dist/storage/d1-driver.js +15 -0
  330. package/dist/storage/keys.js +28 -0
  331. package/dist/storage/sql-driver.js +88 -0
  332. package/dist/storage/sql-native.js +544 -0
  333. package/dist/storage/sql-schema.d.ts +6 -0
  334. package/dist/storage/sql-schema.js +15 -0
  335. package/dist/storage/sql.d.ts +14 -20
  336. package/dist/storage/sql.js +29 -535
  337. package/dist/timeout.js +0 -26
  338. package/dist/tool-safety.js +20 -10
  339. package/dist/types.d.ts +9 -109
  340. package/dist/ui.d.ts +1 -1
  341. package/dist/ui.js +0 -1
  342. package/dist/upward.js +3 -0
  343. package/dist/usage-guide.js +3 -1
  344. package/dist/validate.d.ts +4 -2
  345. package/dist/validate.js +86 -9
  346. package/dist/version.d.ts +1 -1
  347. package/dist/version.js +1 -1
  348. package/dist/worker.js +312 -292
  349. package/dist/workspace-subject.js +5 -1
  350. package/dist/write-outcome.js +454 -34
  351. package/documentation/README.md +28 -7
  352. package/documentation/architecture.md +2409 -255
  353. package/documentation/auth.md +867 -114
  354. package/documentation/code-mode.md +49 -16
  355. package/documentation/deploying.md +164 -20
  356. package/documentation/hosted-mcp-migration.md +460 -0
  357. package/documentation/integrating.md +129 -144
  358. package/documentation/meta-tools.md +409 -32
  359. package/documentation/operating.md +27 -3
  360. package/documentation/operator-tests.md +3 -0
  361. package/documentation/operator-ui.md +84 -48
  362. package/documentation/provider-auth-migration.md +16 -63
  363. package/documentation/provider-migration-0.29.md +10 -5
  364. package/documentation/provider-migration-0.35.md +975 -0
  365. package/documentation/providers.md +1347 -0
  366. package/examples/worker/AGENTS.md +23 -9
  367. package/examples/worker/README.md +254 -135
  368. package/examples/worker/alchemy.run.ts +116 -0
  369. package/examples/worker/gitignore +10 -0
  370. package/examples/worker/src/connecta.config.ts +23 -21
  371. package/examples/worker/wrangler.jsonc +14 -5
  372. package/guides/connecting-agents.md +157 -0
  373. package/guides/how-it-works.md +156 -0
  374. package/guides/safety-and-trust.md +153 -0
  375. package/package.json +30 -50
  376. package/templates/node/.env.example +3 -0
  377. package/templates/node/README.md +22 -2
  378. package/templates/node/docker-compose.yml +1 -0
  379. package/templates/node/package.json +3 -2
  380. package/templates/node/src/connecta.config.ts +8 -0
  381. package/dist/catalog-drift.d.ts +0 -138
  382. package/dist/catalog-drift.js +0 -578
  383. package/dist/operator-ui/agents.d.ts +0 -74
  384. package/dist/operator-ui/agents.js +0 -104
  385. package/dist/operator-ui/config-summary.js +0 -565
  386. package/dist/operator-ui/connection-map.js +0 -376
  387. package/dist/operator-ui/endpoints.js +0 -221
  388. package/dist/operator-ui/fix-prompts.d.ts +0 -18
  389. package/dist/operator-ui/fix-prompts.js +0 -87
  390. package/dist/operator-ui/health.js +0 -52
  391. package/dist/operator-ui/member-page.js +0 -97
  392. package/dist/operator-ui/overview.js +0 -123
  393. package/dist/operator-ui/setup-commands.js +0 -51
  394. package/dist/operator-ui/view.d.ts +0 -401
  395. package/dist/operator-ui/view.js +0 -705
  396. package/dist/providers/_shared/request-token.js +0 -83
  397. package/dist/providers/_shared/rest/dispatch.js +0 -41
  398. package/dist/providers/_shared/rest/operation-index.d.ts +0 -133
  399. package/dist/providers/_shared/rest/tools.d.ts +0 -141
  400. package/dist/providers/_shared/rest/tools.js +0 -569
  401. package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
  402. package/dist/providers/basecamp/index.d.ts +0 -45
  403. package/dist/providers/basecamp/index.js +0 -997
  404. package/dist/providers/basecamp/skill.generated.js +0 -9
  405. package/dist/providers/cloudflare/rest.d.ts +0 -37
  406. package/dist/providers/cloudflare/rest.js +0 -1097
  407. package/dist/providers/github/hosted.js +0 -218
  408. package/dist/providers/github/rest.js +0 -321
  409. package/dist/providers/infisical/index.d.ts +0 -7
  410. package/dist/providers/infisical/index.js +0 -713
  411. package/dist/providers/infisical/skill.generated.js +0 -8
  412. package/dist/providers/intercom/index.d.ts +0 -24
  413. package/dist/providers/intercom/index.js +0 -121
  414. package/dist/providers/intercom/skill.generated.js +0 -8
  415. package/dist/providers/linear/index.d.ts +0 -55
  416. package/dist/providers/linear/index.js +0 -537
  417. package/dist/providers/linear/skill.generated.js +0 -8
  418. package/dist/providers/mixpanel/index.d.ts +0 -59
  419. package/dist/providers/mixpanel/index.js +0 -472
  420. package/dist/providers/mixpanel/operations.js +0 -20
  421. package/dist/providers/mixpanel/rest.js +0 -603
  422. package/dist/providers/mixpanel/skill.generated.js +0 -10
  423. package/dist/providers/mixpanel/value-safety.js +0 -16
  424. package/dist/providers/notion/index.d.ts +0 -67
  425. package/dist/providers/notion/index.js +0 -368
  426. package/dist/providers/notion/openapi.generated.js +0 -75
  427. package/dist/providers/notion/rest.d.ts +0 -24
  428. package/dist/providers/notion/rest.js +0 -1226
  429. package/dist/providers/notion/skill.generated.js +0 -11
  430. package/dist/providers/notion/value-safety.js +0 -56
  431. package/dist/providers/revenuecat/index.d.ts +0 -51
  432. package/dist/providers/revenuecat/index.js +0 -697
  433. package/dist/providers/revenuecat/skill.generated.js +0 -11
  434. package/dist/providers/stripe/rest.d.ts +0 -14
  435. package/dist/providers/stripe/rest.js +0 -402
  436. package/dist/providers/vercel/rest.d.ts +0 -20
  437. package/dist/providers/vercel/rest.js +0 -1020
  438. package/dist/providers/wispr-flow/index.d.ts +0 -14
  439. package/dist/providers/wispr-flow/index.js +0 -117
  440. package/dist/providers/wispr-flow/skill.generated.js +0 -8
  441. /package/dist/providers/github/{key.js → lib/key.js} +0 -0
@@ -0,0 +1,1968 @@
1
+ // Google Slides named workflows on real kit schemas, with bound content continuation.
2
+ import { Effect, Option, Schema } from "effect";
3
+ import { ConnectorCallError } from "../../errors.js";
4
+ import { RESULT_BUDGET_BYTES, clampRedactedText, clampText, jsonBytes } from "../_shared/google/result-size.js";
5
+ import { googleClient, googleOutcomeOf, googleFailureMessage } from "../_shared/google/kit.js";
6
+ import { encodeForAgent, redactBeforeCut, redactSentSecrets } from "../../sent-secrets.js";
7
+ import { REQUEST_KINDS, COMMENT_KINDS, COMMENTS_UNSAVED } from "./lib/requests.js";
8
+ import { SLIDES_INDEX } from "./api.js";
9
+ import * as contracts from "./lib/schemas.js";
10
+ import { read, write } from "../../http/tools.js";
11
+ import { Operation } from "../../runtime/call.js";
12
+ import { fieldsInput, fieldsProblem, selectFields } from "../../http/fields.js";
13
+ import { Refusal } from "../../http/errors.js";
14
+ import { boundCursor } from "../../http/paging.js";
15
+ /** Slides per page of get_presentation. Slides itself returns the whole deck. */
16
+ const DEFAULT_PAGE_SIZE = 20;
17
+ /** Elements per page of get_page, and rows per page of list_layouts. */
18
+ const MAX_ROWS = 500;
19
+ const DEFAULT_ELEMENTS = 50;
20
+ const DEFAULT_LAYOUTS = 100;
21
+ /** Text kept per slide (its elements together, and its notes apart). */
22
+ const DEFAULT_SLIDE_CHARS = 4_000;
23
+ /** Requests per raw batch: connecta's bound; Slides publishes none. */
24
+ /**
25
+ * Every kind of Request Slides' batchUpdate accepts, from its Discovery
26
+ * document's `Request` schema. The raw hatch checks each request's one key
27
+ * against this list, and nothing inside it: an unknown kind is a typo or a
28
+ * new kind to review here, and refusing it locally costs no round trip. The
29
+ * contents are Slides' to validate. Too many names for a schema enum inside
30
+ * the H7 budget, so the handler holds the list. A kind Google adds moves
31
+ * the batchUpdate contract `src/providers/slides/drift.json` records.
32
+ */
33
+ /** What get_presentation's first page may spend on its layout preview. */
34
+ const MAX_LAYOUT_PREVIEW_BYTES = 24 * 1024;
35
+ /** Byte bounds for short labels: names, titles, alt text. */
36
+ const MAX_NAME_BYTES = 256;
37
+ const MAX_TITLE_BYTES = 4 * 1024;
38
+ const MAX_ALT_TEXT_BYTES = 4 * 1024;
39
+ /** A slide title in get_presentation, in characters, before maxCharsPerSlide. */
40
+ const MAX_TITLE_CHARS = 500;
41
+ /** The longest thumbnail link passed on; a real one is well under 2 KB. */
42
+ const MAX_URL_BYTES = 16 * 1024;
43
+ /** One batchUpdate reply kept whole up to this; a larger one is summarized. */
44
+ const MAX_REPLY_BYTES = 2 * 1024;
45
+ /** How deep, and how many array items, a reply summary walks. */
46
+ const MAX_REPLY_DEPTH = 12;
47
+ const MAX_REPLY_ITEMS = 20;
48
+ /** An id or revision longer than this is not copied into a result. */
49
+ const MAX_ID_BYTES = 1024;
50
+ /** replace_all_text echoes each find this far, in order with its count. */
51
+ const MAX_ECHO_CHARS = 100;
52
+ /**
53
+ * The comment kinds of Slides' Request. A batch carrying any of them has
54
+ * comment changes Slides saves apart from the deck's, and only `ALL_SAVED`
55
+ * says they all landed (#696).
56
+ */
57
+ /** The comment save states that say every comment change in a batch landed, or none was asked for. */
58
+ const COMMENTS_SETTLED = new Set(["NO_UPDATES_REQUESTED", "ALL_SAVED"]);
59
+ /**
60
+ * `commentUpdateState`'s zero value. ProtoJSON omits a zero enum, so a batch
61
+ * with comment requests whose reply has no state is reported as this one:
62
+ * Slides did not say the comments saved.
63
+ */
64
+ const COMMENT_STATE_UNSPECIFIED = "COMMENT_UPDATE_STATE_UNSPECIFIED";
65
+ /**
66
+ * The only view mode that cannot hide comments silently. The default omits
67
+ * them, and DEFAULT_FOR_CURRENT_ACCESS omits them for a view-only account,
68
+ * which would read as a deck with none; INCLUDED refuses that account with a
69
+ * 403 instead.
70
+ */
71
+ const COMMENTS_INCLUDED = "COMMENTS_VIEW_MODE_INCLUDED";
72
+ /** Slides' cap on a post's text and an assignee's address, in UTF-8 bytes. */
73
+ const MAX_COMMENT_BYTES = 2048;
74
+ /** Comment threads per page of list_comments. */
75
+ const DEFAULT_THREADS = 50;
76
+ /** A post's text in a write result; Slides caps what it accepts well below this. */
77
+ const MAX_POST_BYTES = 8 * 1024;
78
+ /** EMU per point, the two units a Slides transform uses. */
79
+ const EMU_PER_PT = 12_700;
80
+ /**
81
+ * The partial responses the projected reads ask for: ids, names, and text,
82
+ * without the styles that are most of a deck's bytes. Each element kind keeps
83
+ * one field Slides always sets, so its presence survives the mask. Groups stay
84
+ * whole, because a mask cannot recurse into their children. get_presentation
85
+ * and get_page ask for the same element fields, so both read a page's
86
+ * elements identically — the same reading order, the same text — and a
87
+ * cursor one issues the other honors.
88
+ */
89
+ const TEXT_ONLY = "text(textElements(textRun(content),autoText(content)))";
90
+ const ELEMENT_FIELDS = "objectId,title,description,transform,elementGroup,line(lineType),image(contentUrl),video(source)," +
91
+ `sheetsChart(spreadsheetId),wordArt,table(rows,columns,tableRows(tableCells(${TEXT_ONLY}))),` +
92
+ `shape(shapeType,placeholder(type,index,parentObjectId),${TEXT_ONLY})`;
93
+ const LAYOUT_FIELDS = "masters(objectId,masterProperties(displayName)),layouts(objectId,layoutProperties(name,displayName,masterObjectId))," +
94
+ "notesMaster(objectId)";
95
+ const PRESENTATION_FIELDS = [
96
+ "presentationId",
97
+ "title",
98
+ "revisionId",
99
+ "locale",
100
+ "pageSize",
101
+ LAYOUT_FIELDS,
102
+ `slides(objectId,slideProperties(layoutObjectId,isSkipped,notesPage(objectId,notesProperties,pageElements(${ELEMENT_FIELDS}))),` +
103
+ `pageElements(${ELEMENT_FIELDS}))`,
104
+ ].join(",");
105
+ const PAGE_FIELDS = [
106
+ "objectId",
107
+ "pageType",
108
+ "revisionId",
109
+ "slideProperties(layoutObjectId,masterObjectId,isSkipped,notesPage(objectId))",
110
+ "layoutProperties(name,displayName,masterObjectId)",
111
+ "masterProperties(displayName)",
112
+ "notesProperties(speakerNotesObjectId)",
113
+ `pageElements(${ELEMENT_FIELDS})`,
114
+ ].join(",");
115
+ const LAYOUT_LIST_FIELDS = `presentationId,revisionId,${LAYOUT_FIELDS}`;
116
+ /**
117
+ * A deck's threads, and the anchors on every page a comment can sit on that
118
+ * place them: slides, their notes pages, layouts, masters, the notes master.
119
+ */
120
+ const COMMENT_DECK_FIELDS = "presentationId,revisionId,commentsViewMode,comments," +
121
+ "slides(objectId,commentAnchors,slideProperties(notesPage(objectId,commentAnchors)))," +
122
+ "layouts(objectId,commentAnchors),masters(objectId,commentAnchors),notesMaster(objectId,commentAnchors)";
123
+ const COMMENT_PAGE_FIELDS = "objectId,revisionId,commentsViewMode,comments,commentAnchors";
124
+ function asRecord(value) {
125
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
126
+ }
127
+ function asArray(value) {
128
+ return Array.isArray(value) ? value : [];
129
+ }
130
+ function compact(value) {
131
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
132
+ }
133
+ function text(value) {
134
+ return typeof value === "string" && value !== "" ? value : undefined;
135
+ }
136
+ function editUrl(presentationId) {
137
+ return `https://docs.google.com/presentation/d/${encodeURIComponent(presentationId)}/edit`;
138
+ }
139
+ // --- Result size --------------------------------------------------------------------
140
+ /**
141
+ * The postcondition every read result meets: one result, under the shared
142
+ * budget, whichever way it was called. Each read is built to fit, so this
143
+ * failing means a shape the building missed; it refuses, explicitly, rather
144
+ * than hand back a result neither delivery route can carry.
145
+ */
146
+ function deliverable(result, tool, advice = "read a smaller part, or through slides_api_read, which pages in chunks") {
147
+ const bytes = jsonBytes(result);
148
+ if (bytes > RESULT_BUDGET_BYTES) {
149
+ throw new ConnectorCallError("connector_call_failed", `${tool} built a result of ${bytes} bytes, more than one result can carry (${RESULT_BUDGET_BYTES}). It only read, so nothing was changed; ${advice}.`, { retryable: false });
150
+ }
151
+ return result;
152
+ }
153
+ /**
154
+ * The same postcondition for a write's result, which comes after Google
155
+ * answered 2xx: the write has applied, so a result too large to deliver is
156
+ * refused with that said, and with where to see the outcome — never as if
157
+ * nothing changed, and never as something to retry.
158
+ */
159
+ function appliedDeliverable(result, tool, reread) {
160
+ const bytes = jsonBytes(result);
161
+ if (bytes > RESULT_BUDGET_BYTES)
162
+ throw appliedButUnreadable(tool, `its result is ${bytes} bytes, more than one result can carry`, reread);
163
+ return result;
164
+ }
165
+ function appliedButUnreadable(tool, why, reread) {
166
+ return new ConnectorCallError("connector_call_failed", `${tool} applied — Google answered 2xx — but ${why}. Do not repeat it; ${reread}.`, { retryable: false });
167
+ }
168
+ /** A label cut to `maxBytes` of JSON, saying where the whole of it is. */
169
+ function label(ctx, value, maxBytes, whole) {
170
+ return value === undefined
171
+ ? undefined
172
+ : clampText(ctx, value, maxBytes, (dropped) => `[… ${dropped} more characters; ${whole}]`);
173
+ }
174
+ /** The first `max` UTF-16 units of `value`, never ending half a surrogate pair. */
175
+ function headOf(value, max) {
176
+ if (value.length <= max)
177
+ return value;
178
+ const code = value.charCodeAt(max - 1);
179
+ return value.slice(0, code >= 0xd800 && code <= 0xdbff ? max - 1 : max);
180
+ }
181
+ /**
182
+ * The longest prefix of `value` whose JSON fits `maxBytes` together with the
183
+ * marker its cut earns, as text and as the number of characters it keeps.
184
+ * `value` is text whose sent secrets were replaced before any page of it was
185
+ * cut, so a continuation's offsets count the same text.
186
+ */
187
+ function prefixWithin(value, maxBytes, marker) {
188
+ let dropped = 0;
189
+ const clamped = clampRedactedText(value, maxBytes, (count) => {
190
+ dropped = count;
191
+ return marker(count);
192
+ });
193
+ if (clamped === value)
194
+ return { text: value, kept: value.length };
195
+ if (clamped === "")
196
+ return { text: "", kept: 0 };
197
+ return { text: clamped, kept: value.length - dropped };
198
+ }
199
+ const CURSOR_TOOLS = {
200
+ deck: "get_presentation",
201
+ layouts: "list_layouts",
202
+ page: "get_page",
203
+ comments: "list_comments",
204
+ };
205
+ function cursorText(cursor) {
206
+ return JSON.stringify(compact(cursor));
207
+ }
208
+ /**
209
+ * A cursor of connecta's claims: the presentation and page the caller named,
210
+ * or a page Slides reported, redacted ({@link redactSentSecrets}); a state
211
+ * that holds Slides' revision only when it carries no sent value
212
+ * ({@link stateOf}); and offsets. An agent decodes it and no later pass reads
213
+ * through its encoding.
214
+ */
215
+ function encodeCursor(ctx, cursor) {
216
+ return encodeForAgent(ctx, cursorText(cursor), { url: true, claims: true });
217
+ }
218
+ /**
219
+ * A cursor this tool issued for this deck and page, in this mode, or a
220
+ * refusal that says which of those it is not. The state is checked later,
221
+ * against the deck as it is read.
222
+ */
223
+ function decodeCursor(value, expected) {
224
+ if (value === undefined)
225
+ return undefined;
226
+ const refuse = (why) => new ConnectorCallError("invalid_args", `${why} Omit cursor to start from the beginning.`);
227
+ let cursor;
228
+ try {
229
+ const base64 = String(value).replace(/-/g, "+").replace(/_/g, "/");
230
+ const binary = atob(base64 + "=".repeat((4 - (base64.length % 4)) % 4));
231
+ cursor = JSON.parse(new TextDecoder().decode(Uint8Array.from(binary, (character) => character.charCodeAt(0))));
232
+ }
233
+ catch {
234
+ throw refuse("cursor is not one this connection issued; pass page.nextCursor back unchanged.");
235
+ }
236
+ const counter = (field) => field === undefined || (Number.isSafeInteger(field) && field >= 0);
237
+ if (!cursor ||
238
+ typeof cursor !== "object" ||
239
+ typeof cursor.p !== "string" ||
240
+ typeof cursor.s !== "string" ||
241
+ !Number.isSafeInteger(cursor.i) ||
242
+ cursor.i < 0 ||
243
+ !counter(cursor.o) ||
244
+ !counter(cursor.j)) {
245
+ throw refuse("cursor is not one this connection issued; pass page.nextCursor back unchanged.");
246
+ }
247
+ if (cursor.k !== expected.k) {
248
+ const tool = Object.hasOwn(CURSOR_TOOLS, cursor.k) ? CURSOR_TOOLS[cursor.k] : undefined;
249
+ throw refuse(tool
250
+ ? `cursor belongs to another tool (${tool}).`
251
+ : "cursor is not one this connection issued; pass page.nextCursor back unchanged.");
252
+ }
253
+ if (cursor.p !== expected.p)
254
+ throw refuse("cursor continues a different presentation.");
255
+ if ((cursor.g ?? undefined) !== expected.g)
256
+ throw refuse("cursor continues a different page.");
257
+ return cursor;
258
+ }
259
+ /**
260
+ * List cursors can bind to the revision. Text offsets also bind to a SHA-256
261
+ * of the redacted representation, even with a revision: a different request's
262
+ * secret set can change that text without changing the deck. Viewers without
263
+ * a revision always bind to the content their paging depends on.
264
+ */
265
+ async function stateOf(ctx, revisionId, fingerprint, bindRepresentation = false) {
266
+ // A revision that holds a sent value is bound by its digest instead: a cursor carries no downstream text.
267
+ if (!bindRepresentation &&
268
+ typeof revisionId === "string" &&
269
+ revisionId !== "" &&
270
+ redactSentSecrets(ctx, revisionId) === revisionId)
271
+ return `r:${revisionId}`;
272
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([revisionId ?? null, fingerprint()]))));
273
+ return `f:${Array.from(digest.subarray(0, 16), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
274
+ }
275
+ /** A cursor's deck changed under it: continuing could skip or repeat. */
276
+ function assertUnchanged(cursor, state, what) {
277
+ if (cursor && cursor.s !== state) {
278
+ throw new ConnectorCallError("conflict", `The ${what} changed since this cursor was issued, so continuing from it could skip or repeat what changed. Start again without a cursor.`);
279
+ }
280
+ }
281
+ /** A shape's or cell's text: runs and auto text in order, soft breaks as newlines. */
282
+ function textOf(ctx, content) {
283
+ let out = "";
284
+ for (const element of asArray(asRecord(content)["textElements"]).map(asRecord)) {
285
+ const run = asRecord(element["textRun"])["content"] ?? asRecord(element["autoText"])["content"];
286
+ if (typeof run === "string")
287
+ out += run;
288
+ }
289
+ return redactBeforeCut(ctx, out).replaceAll("\u000b", "\n").replace(/\s+$/, "");
290
+ }
291
+ /** A table as tab-separated cells, one row per line. */
292
+ function tableText(ctx, table) {
293
+ return asArray(table["tableRows"])
294
+ .map((row) => asArray(asRecord(row)["tableCells"])
295
+ .map((cell) => textOf(ctx, asRecord(cell)["text"]).replace(/[\t\n]+/g, " "))
296
+ .join("\t")
297
+ .replace(/\t+$/, ""))
298
+ .join("\n")
299
+ .replace(/\s+$/, "");
300
+ }
301
+ const IDENTITY = [1, 0, 0, 1, 0, 0];
302
+ /**
303
+ * A transform as a matrix. ProtoJSON omits a zero, and a zero scale is real —
304
+ * a group turned 90° has none — so an omitted coefficient of a present
305
+ * transform is 0. Only a transform that is absent altogether is the identity.
306
+ */
307
+ function matrixOf(transform) {
308
+ if (!transform || typeof transform !== "object" || Array.isArray(transform))
309
+ return IDENTITY;
310
+ const t = transform;
311
+ const n = (value) => (typeof value === "number" && Number.isFinite(value) ? value : 0);
312
+ const unit = t["unit"] === "PT" ? EMU_PER_PT : 1;
313
+ return [
314
+ n(t["scaleX"]),
315
+ n(t["shearY"]),
316
+ n(t["shearX"]),
317
+ n(t["scaleY"]),
318
+ n(t["translateX"]) * unit,
319
+ n(t["translateY"]) * unit,
320
+ ];
321
+ }
322
+ /** `parent` preconcatenated with `child`, as Slides composes group transforms. */
323
+ function compose(parent, child) {
324
+ const [a, b, c, d, e, f] = parent;
325
+ const [ca, cb, cc, cd, ce, cf] = child;
326
+ return [a * ca + c * cb, b * ca + d * cb, a * cc + c * cd, b * cc + d * cd, a * ce + c * cf + e, b * ce + d * cf + f];
327
+ }
328
+ function kindOf(element) {
329
+ if (element["shape"])
330
+ return "shape";
331
+ if (element["table"])
332
+ return "table";
333
+ if (element["wordArt"])
334
+ return "wordArt";
335
+ if (element["image"])
336
+ return "image";
337
+ if (element["video"])
338
+ return "video";
339
+ if (element["sheetsChart"])
340
+ return "chart";
341
+ if (element["line"])
342
+ return "line";
343
+ return "other";
344
+ }
345
+ function collect(ctx, elements, parent, groupId, depth, out) {
346
+ for (const element of asArray(elements).map(asRecord)) {
347
+ const matrix = compose(parent, matrixOf(element["transform"]));
348
+ const group = asRecord(element["elementGroup"]);
349
+ if (Array.isArray(group["children"]) && depth < 20) {
350
+ collect(ctx, group["children"], matrix, text(element["objectId"]), depth + 1, out);
351
+ continue;
352
+ }
353
+ const kind = kindOf(element);
354
+ out.push({
355
+ element,
356
+ groupId,
357
+ kind,
358
+ text: kind === "shape"
359
+ ? textOf(ctx, asRecord(element["shape"])["text"])
360
+ : kind === "table"
361
+ ? tableText(ctx, asRecord(element["table"]))
362
+ : kind === "wordArt"
363
+ ? (text(asRecord(element["wordArt"])["renderedText"]) ?? "")
364
+ : "",
365
+ x: matrix[4],
366
+ y: matrix[5],
367
+ });
368
+ }
369
+ }
370
+ /**
371
+ * A page's leaf elements in reading order: top to bottom, then left to right,
372
+ * by absolute position rounded to the point, so a hairline offset does not
373
+ * reorder a row. The index into this list is what a get_page cursor counts.
374
+ */
375
+ function readingOrder(ctx, elements) {
376
+ const out = [];
377
+ collect(ctx, elements, IDENTITY, undefined, 0, out);
378
+ // Text is cut, and continued from an offset, only with every secret the
379
+ // request sent replaced: a cut through an echo leaves a fragment.
380
+ for (const leaf of out)
381
+ leaf.text = redactBeforeCut(ctx, leaf.text);
382
+ return out.sort((left, right) => Math.round(left.y / EMU_PER_PT) - Math.round(right.y / EMU_PER_PT) ||
383
+ Math.round(left.x / EMU_PER_PT) - Math.round(right.x / EMU_PER_PT));
384
+ }
385
+ /** What a projected page's cursor depends on when there is no revision. */
386
+ function leafFingerprint(leaves) {
387
+ // The text itself, not its length: a continuation resumes at a character
388
+ // offset, and an edit that keeps the length still moves what is there.
389
+ return leaves.map((leaf) => [leaf.element["objectId"] ?? null, leaf.kind, leaf.text]);
390
+ }
391
+ function placeholderOf(leaf) {
392
+ return leaf.kind === "shape" ? asRecord(asRecord(leaf.element["shape"])["placeholder"]) : {};
393
+ }
394
+ function altTextOf(element) {
395
+ return [text(element["title"]), text(element["description"])].filter(Boolean).join(": ") || undefined;
396
+ }
397
+ /** The fields of an element both readers share: what it is, and how to address it. */
398
+ function describeLeaf(ctx, leaf, raw) {
399
+ const placeholder = placeholderOf(leaf);
400
+ return compact({
401
+ objectId: text(leaf.element["objectId"]),
402
+ kind: leaf.kind,
403
+ placeholder: text(placeholder["type"]),
404
+ altText: label(ctx, altTextOf(leaf.element), MAX_ALT_TEXT_BYTES, raw),
405
+ // A linked chart's data lives in Sheets; the id is how to reach it.
406
+ spreadsheetId: leaf.kind === "chart" ? text(asRecord(leaf.element["sheetsChart"])["spreadsheetId"]) : undefined,
407
+ });
408
+ }
409
+ const FULL_STRUCTURE = "slides_api_read has the complete structure";
410
+ // --- get_presentation ---------------------------------------------------------------
411
+ const TITLE_PLACEHOLDERS = new Set(["TITLE", "CENTERED_TITLE"]);
412
+ /** Why text was cut, as the marker tells the reader. */
413
+ const RAISE_CAP = "raise maxCharsPerSlide, or continue with get_page from textCursor";
414
+ const OVER_RESULT = "this slide is larger than one result can carry; continue with get_page from textCursor";
415
+ /**
416
+ * One slide's projection: its elements' text in reading order under a shared
417
+ * per-slide character budget, its speaker notes under the same budget apart,
418
+ * and, for every cut, the get_page cursor that reads on from the cut.
419
+ */
420
+ async function projectSlide(ctx, value, index, deck, maxChars, why = RAISE_CAP) {
421
+ const slide = asRecord(value);
422
+ const slideId = text(slide["objectId"]) ?? "";
423
+ // Slides' ids enter a cursor only redacted, as every downstream value a cursor holds does.
424
+ const cursorPage = redactSentSecrets(ctx, slideId);
425
+ const properties = asRecord(slide["slideProperties"]);
426
+ const leaves = readingOrder(ctx, slide["pageElements"]);
427
+ let state;
428
+ const slideState = () => (state ??= stateOf(ctx, deck.revisionId, () => leafFingerprint(leaves), true));
429
+ let budget = maxChars;
430
+ let truncated = false;
431
+ let omitted = 0;
432
+ let title;
433
+ const elements = [];
434
+ for (const [position, leaf] of leaves.entries()) {
435
+ const placeholder = text(placeholderOf(leaf)["type"]);
436
+ const alt = altTextOf(leaf.element);
437
+ // Decorative lines and empty boxes carry nothing to read; they are
438
+ // counted, not listed. An empty placeholder is kept: its id is where
439
+ // insertText fills a new slide's title or body.
440
+ if (leaf.kind === "line" ||
441
+ (!leaf.text && !alt && !placeholder && (leaf.kind === "shape" || leaf.kind === "wordArt"))) {
442
+ omitted += 1;
443
+ continue;
444
+ }
445
+ if (placeholder && TITLE_PLACEHOLDERS.has(placeholder) && title === undefined && leaf.text) {
446
+ const line = leaf.text.replace(/\n+/g, " ");
447
+ const keep = headOf(line, Math.min(MAX_TITLE_CHARS, maxChars));
448
+ title =
449
+ keep.length < line.length
450
+ ? `${keep}\n[… ${line.length - keep.length} more characters; the title element's text has them]`
451
+ : line;
452
+ }
453
+ const row = describeLeaf(ctx, leaf, FULL_STRUCTURE);
454
+ if (row["altText"] !== alt)
455
+ truncated = true;
456
+ if (leaf.text) {
457
+ const kept = headOf(leaf.text, Math.max(budget, 0));
458
+ budget -= kept.length;
459
+ if (kept.length < leaf.text.length) {
460
+ truncated = true;
461
+ row["text"] = `${kept}\n[… ${leaf.text.length - kept.length} more characters truncated; ${why}]`;
462
+ row["truncated"] = true;
463
+ row["textCursor"] = encodeCursor(ctx, {
464
+ k: "page",
465
+ p: deck.presentationId,
466
+ g: cursorPage,
467
+ s: await slideState(),
468
+ i: position,
469
+ o: kept.length,
470
+ });
471
+ }
472
+ else {
473
+ row["text"] = kept;
474
+ }
475
+ }
476
+ row["position"] = position;
477
+ elements.push(row);
478
+ }
479
+ const notesPage = asRecord(properties["notesPage"]);
480
+ const notesPageId = text(notesPage["objectId"]);
481
+ const notesId = text(asRecord(notesPage["notesProperties"])["speakerNotesObjectId"]);
482
+ const notesLeaves = readingOrder(ctx, notesPage["pageElements"]);
483
+ const notesAt = notesLeaves.findIndex((leaf) => notesId !== undefined && leaf.element["objectId"] === notesId);
484
+ const notesText = notesAt >= 0 ? notesLeaves[notesAt].text : "";
485
+ let notes;
486
+ let notesCursor;
487
+ if (notesText) {
488
+ const kept = headOf(notesText, maxChars);
489
+ if (kept.length < notesText.length) {
490
+ truncated = true;
491
+ notes = `${kept}\n[… ${notesText.length - kept.length} more characters truncated; ${why.replace("textCursor", "notesCursor")}]`;
492
+ notesCursor = encodeCursor(ctx, {
493
+ k: "page",
494
+ p: deck.presentationId,
495
+ g: redactSentSecrets(ctx, notesPageId ?? ""),
496
+ s: await stateOf(ctx, deck.revisionId, () => leafFingerprint(notesLeaves), true),
497
+ i: notesAt,
498
+ o: kept.length,
499
+ });
500
+ }
501
+ else {
502
+ notes = notesText;
503
+ }
504
+ }
505
+ const layoutId = text(properties["layoutObjectId"]);
506
+ return compact({
507
+ objectId: slideId || undefined,
508
+ index,
509
+ layout: layoutId ? deck.layouts.get(layoutId) : undefined,
510
+ layoutId,
511
+ skipped: properties["isSkipped"] === true || undefined,
512
+ title,
513
+ elements,
514
+ omittedElements: omitted > 0 ? omitted : undefined,
515
+ notesPageId,
516
+ notes,
517
+ notesCursor,
518
+ truncated: truncated || undefined,
519
+ // Kept for fitting: where each listed element sits among the page's
520
+ // leaves, which an elementsCursor counts. Removed before returning.
521
+ _state: slideState,
522
+ });
523
+ }
524
+ /** A projected slide as returned: the fitting bookkeeping gone. */
525
+ function finished(slide) {
526
+ const { _state, ...rest } = slide;
527
+ return {
528
+ ...rest,
529
+ elements: asArray(rest["elements"]).map((element) => {
530
+ const { position: _position, ...row } = asRecord(element);
531
+ return row;
532
+ }),
533
+ };
534
+ }
535
+ /**
536
+ * A page's first slide that is too large for the `room` left, made to fit:
537
+ * its text halved until it does, then — for a slide of so many elements that
538
+ * it does not fit with no text — its last elements left out, counted, and
539
+ * named by the get_page cursor that lists them. Every cut says it was the
540
+ * result's size, not maxCharsPerSlide. A later slide that does not fit starts
541
+ * the next page whole instead.
542
+ */
543
+ async function fitSlide(ctx, value, index, deck, maxChars, room) {
544
+ for (let chars = Math.floor(maxChars / 2);; chars = Math.floor(chars / 2)) {
545
+ const slide = await projectSlide(ctx, value, index, deck, chars, OVER_RESULT);
546
+ if (jsonBytes(finished(slide)) <= room)
547
+ return finished(slide);
548
+ if (chars > 0)
549
+ continue;
550
+ const elements = asArray(slide["elements"]).map(asRecord);
551
+ const state = await slide["_state"]();
552
+ const keeping = (count) => finished({
553
+ ...slide,
554
+ elements: elements.slice(0, count),
555
+ elementsNotShown: elements.length - count,
556
+ elementsCursor: encodeCursor(ctx, {
557
+ k: "page",
558
+ p: deck.presentationId,
559
+ g: redactSentSecrets(ctx, text(asRecord(value)["objectId"]) ?? ""),
560
+ s: state,
561
+ i: count < elements.length ? Number(elements[count]["position"]) : 0,
562
+ }),
563
+ truncated: true,
564
+ });
565
+ let low = 0;
566
+ let high = elements.length;
567
+ while (low < high) {
568
+ const middle = Math.ceil((low + high) / 2);
569
+ if (jsonBytes(keeping(middle)) <= room)
570
+ low = middle;
571
+ else
572
+ high = middle - 1;
573
+ }
574
+ // The result's postcondition refuses a slide that still does not fit.
575
+ return keeping(low);
576
+ }
577
+ }
578
+ // --- Layouts ------------------------------------------------------------------------
579
+ /** Masters, each followed by its layouts, as rows create_slide and get_page take ids from. */
580
+ function layoutRows(ctx, presentation) {
581
+ const whole = "get_page on this id through slides_api_read has the whole name";
582
+ const masters = asArray(presentation["masters"]).map(asRecord);
583
+ const layouts = asArray(presentation["layouts"]).map(asRecord);
584
+ const layoutRow = (layout) => {
585
+ const properties = asRecord(layout["layoutProperties"]);
586
+ return compact({
587
+ objectId: text(layout["objectId"]),
588
+ kind: "layout",
589
+ name: label(ctx, text(properties["name"]), MAX_NAME_BYTES, whole),
590
+ displayName: label(ctx, text(properties["displayName"]), MAX_NAME_BYTES, whole),
591
+ masterId: text(properties["masterObjectId"]),
592
+ });
593
+ };
594
+ const rows = [];
595
+ const placed = new Set();
596
+ for (const master of masters) {
597
+ const id = text(master["objectId"]);
598
+ rows.push(compact({
599
+ objectId: id,
600
+ kind: "master",
601
+ displayName: label(ctx, text(asRecord(master["masterProperties"])["displayName"]), MAX_NAME_BYTES, whole),
602
+ }));
603
+ for (const layout of layouts) {
604
+ if (id !== undefined && asRecord(layout["layoutProperties"])["masterObjectId"] === id) {
605
+ rows.push(layoutRow(layout));
606
+ placed.add(layout);
607
+ }
608
+ }
609
+ }
610
+ for (const layout of layouts)
611
+ if (!placed.has(layout))
612
+ rows.push(layoutRow(layout));
613
+ return rows;
614
+ }
615
+ /** What a list_layouts cursor depends on when there is no revision. */
616
+ function layoutFingerprint(rows) {
617
+ return rows.map((row) => row["objectId"] ?? null);
618
+ }
619
+ /** A layout's name as a person picks it, bounded: its display name, else its kind. */
620
+ function layoutNames(ctx, presentation) {
621
+ const names = new Map();
622
+ for (const layout of asArray(presentation["layouts"]).map(asRecord)) {
623
+ const id = text(layout["objectId"]);
624
+ const properties = asRecord(layout["layoutProperties"]);
625
+ const name = label(ctx, text(properties["displayName"]) ?? text(properties["name"]), MAX_NAME_BYTES, "get_page on layoutId through slides_api_read has the whole name");
626
+ if (id && name)
627
+ names.set(id, name);
628
+ }
629
+ return names;
630
+ }
631
+ /**
632
+ * Rows from `start` that fit `room` bytes and `limit` rows, as a page of
633
+ * them and the index the next page starts at.
634
+ */
635
+ function rowsWithin(rows, start, limit, room) {
636
+ const out = [];
637
+ let used = 2;
638
+ let next = start;
639
+ while (next < rows.length && out.length < limit) {
640
+ const size = jsonBytes(rows[next]) + 1;
641
+ if (used + size > room)
642
+ break;
643
+ out.push(rows[next]);
644
+ used += size;
645
+ next += 1;
646
+ }
647
+ return { rows: out, next };
648
+ }
649
+ function dimension(value) {
650
+ const record = asRecord(value);
651
+ return typeof record["magnitude"] === "number"
652
+ ? { magnitude: record["magnitude"], unit: text(record["unit"]) ?? "EMU" }
653
+ : undefined;
654
+ }
655
+ /** The size of a page object carrying the longest cursor a result may hold. */
656
+ function pageEnvelopeBytes(cursor) {
657
+ const longest = { ...cursor, i: 999_999, o: 99_999_999, j: cursor.j === undefined ? undefined : 999_999 };
658
+ const units = Math.ceil((new TextEncoder().encode(cursorText(longest)).length * 4) / 3);
659
+ return 1024 + jsonBytes({ page: { hasMore: true, nextCursor: "x".repeat(units) } });
660
+ }
661
+ // --- get_page -------------------------------------------------------------------------
662
+ /** What get_page says about the page itself, by the page's kind. */
663
+ function describePage(ctx, page) {
664
+ const whole = "slides_api_read has the whole name";
665
+ const slide = asRecord(page["slideProperties"]);
666
+ const layout = asRecord(page["layoutProperties"]);
667
+ return compact({
668
+ pageType: text(page["pageType"]),
669
+ layoutId: text(slide["layoutObjectId"]),
670
+ masterId: text(slide["masterObjectId"]) ?? text(layout["masterObjectId"]),
671
+ notesPageId: text(asRecord(slide["notesPage"])["objectId"]),
672
+ skipped: slide["isSkipped"] === true || undefined,
673
+ speakerNotesObjectId: text(asRecord(page["notesProperties"])["speakerNotesObjectId"]),
674
+ name: label(ctx, text(layout["name"]), MAX_NAME_BYTES, whole),
675
+ displayName: label(ctx, text(layout["displayName"]) ?? text(asRecord(page["masterProperties"])["displayName"]), MAX_NAME_BYTES, whole),
676
+ });
677
+ }
678
+ /**
679
+ * Projected rows of a page from element `i`, text offset `o`: every leaf,
680
+ * in reading order, with its text continued across pages wherever one page
681
+ * cannot hold it.
682
+ */
683
+ function projectedRows(ctx, leaves, start, limit, room) {
684
+ const elements = [];
685
+ let used = 2;
686
+ let index = start.i;
687
+ let offset = start.o;
688
+ while (index < leaves.length && elements.length < limit) {
689
+ const leaf = leaves[index];
690
+ const placeholder = placeholderOf(leaf);
691
+ const row = {
692
+ ...describeLeaf(ctx, leaf, FULL_STRUCTURE),
693
+ ...compact({
694
+ groupId: leaf.groupId,
695
+ placeholderIndex: typeof placeholder["index"] === "number" ? placeholder["index"] : undefined,
696
+ placeholderParentId: text(placeholder["parentObjectId"]),
697
+ textOffset: offset > 0 ? offset : undefined,
698
+ }),
699
+ };
700
+ const rest = leaf.text.slice(offset);
701
+ if (!rest) {
702
+ const size = jsonBytes(row) + 1;
703
+ if (used + size > room)
704
+ break;
705
+ elements.push(row);
706
+ used += size;
707
+ index += 1;
708
+ offset = 0;
709
+ continue;
710
+ }
711
+ // The row as it would be if its text were cut, so the text's share is
712
+ // measured against everything else the row carries.
713
+ const shell = { ...row, text: "", truncated: true, textLength: leaf.text.length };
714
+ const available = room - used - 1 - (jsonBytes(shell) - 2);
715
+ const cut = available > 2
716
+ ? prefixWithin(rest, available, (dropped) => `\n[… ${dropped} more characters continue on the next page]`)
717
+ : { text: "", kept: 0 };
718
+ if (cut.kept === 0 && elements.length > 0)
719
+ break;
720
+ if (cut.kept === rest.length) {
721
+ const whole = { ...row, text: rest };
722
+ used += jsonBytes(whole) + 1;
723
+ elements.push(whole);
724
+ index += 1;
725
+ offset = 0;
726
+ continue;
727
+ }
728
+ if (cut.kept === 0) {
729
+ throw new ConnectorCallError("connector_call_failed", "get_page could not fit any of an element's text in one result.", { retryable: false });
730
+ }
731
+ elements.push({ ...shell, text: cut.text });
732
+ return { elements, next: { i: index, o: offset + cut.kept } };
733
+ }
734
+ return { elements, next: index < leaves.length ? { i: index, o: offset } : undefined };
735
+ }
736
+ // --- Writing ----------------------------------------------------------------------
737
+ /**
738
+ * One batchUpdate. A write that names `requiredRevisionId` is revision
739
+ * guarded: the shared layer turns Slides' precondition refusal into a
740
+ * `conflict`, so a deck someone changed since the read is re-read, not
741
+ * retried. Nothing was applied in that case: a batch Slides refuses applies
742
+ * none of its requests. One it accepts applies its deck changes together,
743
+ * and its comment changes apart (see {@link commentSaveFields}).
744
+ */
745
+ function batchUpdate(client, presentationId, requests, requiredRevisionId) {
746
+ return Effect.gen(function* () {
747
+ return asRecord(yield* client.json({
748
+ method: "POST",
749
+ template: "/presentations/{presentationId}:batchUpdate",
750
+ path: `/presentations/${encodeURIComponent(presentationId)}:batchUpdate`,
751
+ body: compact({
752
+ requests,
753
+ writeControl: requiredRevisionId === undefined ? undefined : { requiredRevisionId },
754
+ }),
755
+ }, { revisionGuarded: requiredRevisionId !== undefined }));
756
+ });
757
+ }
758
+ /**
759
+ * A create whose outcome Google left uncertain — sent with no answer back, a
760
+ * redirect, a server failure after it was sent, or a 2xx whose answer broke —
761
+ * keeps the shared verdict and adds what to look for before creating again,
762
+ * since a second create makes a second deck or slide. Only a request that
763
+ * never left, or that Google explicitly refused, is known not to have
764
+ * applied, so every other dispatched outcome gets the advice, whatever phase
765
+ * name the shared layer gives it. The advice claims nothing about whether it
766
+ * applied; the shared verdict says that, and says "probably applied" only
767
+ * after a recorded 2xx. Any other failure passes through.
768
+ */
769
+ function afterUnknownCreate(connection, failure, lookFor) {
770
+ const outcome = googleOutcomeOf(failure);
771
+ return outcome?.dispatched === true && outcome.phase !== "refused"
772
+ ? googleFailureMessage(connection, failure, (message) => `${message} ${lookFor}`)
773
+ : Effect.fail(failure);
774
+ }
775
+ /**
776
+ * A field that names something to address later: objectId, commentId,
777
+ * postId, anchorId, … — and a post author's `user`, the `users/{id}` that
778
+ * identifies who wrote it.
779
+ */
780
+ const ID_KEY = /^(id|user|[a-z][A-Za-z0-9]*Id)$/;
781
+ /**
782
+ * An id as copied from Google into a result: whole, or not at all. A cut id
783
+ * names nothing, so one too long to return is dropped and flagged instead.
784
+ */
785
+ function wholeId(value) {
786
+ return typeof value === "string" && value !== "" && jsonBytes(value) <= MAX_ID_BYTES ? value : undefined;
787
+ }
788
+ /**
789
+ * `value` keeping, at any depth, every id whole, every number and boolean,
790
+ * and — in "short" mode — strings short enough to read at a glance; every
791
+ * path left out goes to `cut`. Every array item is walked in both modes, so
792
+ * no id is dropped to save space — the result's budget, which counts what
793
+ * does not fit, handles that; past the first few items, "short" mode keeps
794
+ * only their ids.
795
+ * Nesting deeper than any reply Slides sends is cut where it starts.
796
+ */
797
+ function pruned(value, key, path, mode, cut, depth) {
798
+ if (value === null || typeof value === "number" || typeof value === "boolean")
799
+ return value;
800
+ if (typeof value === "string") {
801
+ const keep = ID_KEY.test(key)
802
+ ? wholeId(value) !== undefined
803
+ : mode === "short" && jsonBytes(value) <= MAX_NAME_BYTES;
804
+ if (keep)
805
+ return value;
806
+ cut.push(path);
807
+ return undefined;
808
+ }
809
+ if (typeof value !== "object" || depth >= MAX_REPLY_DEPTH) {
810
+ cut.push(path);
811
+ return undefined;
812
+ }
813
+ if (Array.isArray(value)) {
814
+ // Every item is walked; past the first few, only for ids, whose cut
815
+ // fields are summed up in one entry rather than one per item.
816
+ if (mode === "short" && value.length > MAX_REPLY_ITEMS) {
817
+ cut.push(`${path}[${MAX_REPLY_ITEMS}…${value.length - 1}] (ids only)`);
818
+ }
819
+ return value.map((item, index) => {
820
+ const itemMode = mode === "short" && index >= MAX_REPLY_ITEMS ? "ids" : mode;
821
+ const sink = itemMode === mode ? cut : [];
822
+ return pruned(item, key, `${path}[${index}]`, itemMode, sink, depth + 1) ?? null;
823
+ });
824
+ }
825
+ const out = {};
826
+ for (const [field, item] of Object.entries(value)) {
827
+ const kept = pruned(item, field, path ? `${path}.${field}` : field, mode, cut, depth + 1);
828
+ if (kept !== undefined)
829
+ out[field] = kept;
830
+ }
831
+ return out;
832
+ }
833
+ /** The paths a summary left out, bounded: the first ten, then a count. */
834
+ function cutList(ctx, cut) {
835
+ const named = cut.slice(0, 10).map((path) => clampText(ctx, path, 160, () => "…"));
836
+ return cut.length > named.length ? [...named, `(${cut.length - named.length} more)`] : named;
837
+ }
838
+ /**
839
+ * One batchUpdate reply, small enough to return. Slides' replies are tiny —
840
+ * a new object's id, a count — and pass whole. One that is not is walked:
841
+ * every id at every depth is kept whole, then short fields while they fit,
842
+ * and every path left out is named in `cut`, so nothing is dropped without a
843
+ * word.
844
+ */
845
+ function summarizeReply(ctx, reply) {
846
+ if (jsonBytes(reply) <= MAX_REPLY_BYTES)
847
+ return reply;
848
+ let summary = {};
849
+ for (const mode of ["short", "ids"]) {
850
+ const cut = [];
851
+ summary = { ...asRecord(pruned(reply, "", "", mode, cut, 0)), cut: cutList(ctx, cut) };
852
+ if (jsonBytes(summary) <= MAX_REPLY_BYTES)
853
+ return summary;
854
+ }
855
+ // Ids alone over the per-reply share: still returned, since what a write
856
+ // created is the one thing a caller cannot ask for again.
857
+ return summary;
858
+ }
859
+ /**
860
+ * The most one host result may carry into an execute_code program
861
+ * (`MAX_HOST_RESULT_BYTES` in `src/executors/quickjs-runtime.ts`). A write
862
+ * result past the provider's own 192 KiB budget but under this still reaches
863
+ * a program; past it, only a direct call does, paging with connecta.result.
864
+ */
865
+ const BRIDGE_BYTES = 256 * 1024;
866
+ /**
867
+ * The most a raw batch's result may be when the ids its replies carry
868
+ * outgrow one result. Every id is still returned, never dropped: a direct
869
+ * call stashes a result this size and pages it with connecta.result (the stash
870
+ * holds 8 MiB by default). Past this, the ids are not returned at all, and
871
+ * the result says where to read them.
872
+ */
873
+ const MAX_IDS_RESULT_BYTES = 4 * 1024 * 1024;
874
+ /**
875
+ * A reply at its smallest, however small it already is: every id at every
876
+ * depth with its numbers and booleans, and how many fields were left out.
877
+ * A reply with nothing but those passes unchanged.
878
+ */
879
+ function idsOnlyReply(reply) {
880
+ const cut = [];
881
+ const ids = asRecord(pruned(reply, "", "", "ids", cut, 0));
882
+ return cut.length === 0 ? reply : { ...ids, cut: [`(${cut.length} fields left out; ids only)`] };
883
+ }
884
+ /**
885
+ * A batch's replies, in order, after `used` bytes: ids first, then text.
886
+ * Every reply starts at its ids alone, and replies are then given their
887
+ * fuller summaries, in order, while the provider's 192 KiB budget holds
888
+ * them, so text is cut before any id is. Should the ids alone outgrow that
889
+ * budget, they are all returned anyway (`oversized`) and no text is; nothing
890
+ * a write created is ever dropped to save space.
891
+ */
892
+ function boundedReplies(ctx, replies, used) {
893
+ const out = replies.map((reply) => idsOnlyReply(asRecord(reply)));
894
+ const sizes = out.map((reply) => jsonBytes(reply) + 1);
895
+ let total = used + 2 + sizes.reduce((sum, size) => sum + size, 0);
896
+ if (total > RESULT_BUDGET_BYTES)
897
+ return { replies: out, oversized: true };
898
+ for (const [index, reply] of replies.entries()) {
899
+ const fuller = summarizeReply(ctx, asRecord(reply));
900
+ const size = jsonBytes(fuller) + 1;
901
+ if (total - sizes[index] + size > RESULT_BUDGET_BYTES)
902
+ continue;
903
+ total += size - sizes[index];
904
+ out[index] = fuller;
905
+ sizes[index] = size;
906
+ }
907
+ return { replies: out, oversized: false };
908
+ }
909
+ /**
910
+ * The revision a write left the deck at, for the next write to name — whole,
911
+ * or flagged as not shown when too long to copy, never cut.
912
+ */
913
+ function revisionFields(response) {
914
+ const revision = asRecord(response["writeControl"])["requiredRevisionId"];
915
+ const whole = wholeId(revision);
916
+ if (whole !== undefined)
917
+ return { revisionId: whole };
918
+ return typeof revision === "string" && revision !== "" ? { revisionIdNotShown: true } : {};
919
+ }
920
+ // --- Comments ---------------------------------------------------------------------
921
+ /**
922
+ * What a batch's comment changes came to, as a result field. A batch with no
923
+ * comment request is settled unless Slides says otherwise. One with any is
924
+ * settled only on `ALL_SAVED`: anything else — a failure, or no state at all —
925
+ * is surfaced by name, since its deck changes may have applied while its
926
+ * comments did not, and the batch is then neither all nor none.
927
+ */
928
+ function commentSaveFields(ctx, response, sentComments) {
929
+ const state = text(response["commentUpdateState"]);
930
+ const settled = sentComments ? state === "ALL_SAVED" : state === undefined || COMMENTS_SETTLED.has(state);
931
+ if (settled)
932
+ return {};
933
+ return {
934
+ commentUpdateState: label(ctx, state ?? COMMENT_STATE_UNSPECIFIED, MAX_NAME_BYTES, "Slides sent a longer state") ?? "",
935
+ };
936
+ }
937
+ /** Comment actions that change nothing, and are left unsaid. */
938
+ const QUIET_ACTIONS = new Set(["COMMENT_ACTION_TYPE_UNSPECIFIED", "NO_COMMENT_ACTION_CHANGE"]);
939
+ /**
940
+ * A post as returned: its id and its author's `users/{id}` whole — left out
941
+ * and flagged on the thread's `idsNotShown`, never cut, should either be too
942
+ * long to copy — beside the display name, which is not an identity.
943
+ * `contentHtml` is the same text rendered, and is left behind.
944
+ */
945
+ function postPart(ctx, value) {
946
+ const post = asRecord(value);
947
+ const author = asRecord(post["author"]);
948
+ const postId = wholeId(post["postId"]);
949
+ const user = wholeId(author["user"]);
950
+ const shownAuthor = compact({
951
+ user,
952
+ displayName: label(ctx, text(author["displayName"]), MAX_NAME_BYTES, "Slides shows the whole name"),
953
+ me: author["me"] === true || undefined,
954
+ anonymous: author["anonymous"] === true || undefined,
955
+ });
956
+ const action = text(post["commentAction"]);
957
+ const whole = "Slides shows all of it";
958
+ return {
959
+ row: compact({
960
+ postId,
961
+ author: Object.keys(shownAuthor).length > 0 ? shownAuthor : undefined,
962
+ commentAction: action && !QUIET_ACTIONS.has(action) ? label(ctx, action, MAX_NAME_BYTES, whole) : undefined,
963
+ assigneeEmail: label(ctx, text(post["assigneeEmail"]), MAX_COMMENT_BYTES + 64, whole),
964
+ createTime: label(ctx, text(post["createTime"]), MAX_NAME_BYTES, whole),
965
+ updateTime: label(ctx, text(post["updateTime"]), MAX_NAME_BYTES, whole),
966
+ deleted: post["deleted"] === true || undefined,
967
+ // Slides gives an imported post no author id; this says why.
968
+ imported: post["fromImportedPresentation"] === true || undefined,
969
+ }),
970
+ // Paged from offsets, so replaced before any cut (see readingOrder).
971
+ content: typeof post["content"] === "string" ? redactBeforeCut(ctx, post["content"]) : "",
972
+ idsNotShown: (text(post["postId"]) !== undefined && postId === undefined) ||
973
+ (text(author["user"]) !== undefined && user === undefined),
974
+ };
975
+ }
976
+ /** A post in a write result: its text bounded, never its ids. */
977
+ function writtenPost(ctx, value, maxBytes = MAX_POST_BYTES) {
978
+ const post = postPart(ctx, value);
979
+ return compact({
980
+ ...post.row,
981
+ content: label(ctx, text(post.content), maxBytes, "list_comments has the whole post"),
982
+ });
983
+ }
984
+ function anchorPlaces(pages) {
985
+ const places = new Map();
986
+ for (const page of pages) {
987
+ const pageId = text(page["objectId"]);
988
+ for (const anchor of asArray(page["commentAnchors"]).map(asRecord)) {
989
+ const anchorId = text(anchor["anchorId"]);
990
+ if (anchorId === undefined)
991
+ continue;
992
+ let place = places.get(anchorId);
993
+ if (!place)
994
+ places.set(anchorId, (place = { pages: new Set(), objects: new Set() }));
995
+ if (pageId)
996
+ place.pages.add(pageId);
997
+ for (const object of asArray(anchor["objectAnchors"]).map(asRecord)) {
998
+ // An anchor on the page itself names the page, already listed.
999
+ const objectId = text(object["objectId"]);
1000
+ if (objectId && objectId !== pageId)
1001
+ place.objects.add(objectId);
1002
+ }
1003
+ }
1004
+ }
1005
+ return places;
1006
+ }
1007
+ /** Every page a deck read returns anchors on: slides, notes pages, layouts, masters, the notes master. */
1008
+ function commentPages(presentation) {
1009
+ const slides = asArray(presentation["slides"]).map(asRecord);
1010
+ return [
1011
+ ...slides,
1012
+ ...slides.map((slide) => asRecord(asRecord(slide["slideProperties"])["notesPage"])),
1013
+ ...asArray(presentation["layouts"]).map(asRecord),
1014
+ ...asArray(presentation["masters"]).map(asRecord),
1015
+ asRecord(presentation["notesMaster"]),
1016
+ ];
1017
+ }
1018
+ function threadParts(ctx, value, places) {
1019
+ const thread = asRecord(value);
1020
+ const commentId = wholeId(thread["commentId"]);
1021
+ const anchorId = wholeId(thread["anchorId"]);
1022
+ const place = anchorId === undefined ? undefined : places.get(anchorId);
1023
+ const ids = (values) => [...(values ?? [])].map(wholeId).filter((id) => id !== undefined);
1024
+ const pageIds = ids(place?.pages);
1025
+ const objectIds = ids(place?.objects);
1026
+ const replies = asArray(thread["replies"]).map((reply) => postPart(ctx, reply));
1027
+ const head = thread["headPost"] && typeof thread["headPost"] === "object" ? postPart(ctx, thread["headPost"]) : undefined;
1028
+ return {
1029
+ row: compact({
1030
+ commentId,
1031
+ anchorId,
1032
+ status: label(ctx, text(thread["status"]), MAX_NAME_BYTES, "Slides shows the status"),
1033
+ // Every id, as everywhere: Slides anchors a group's comment to at most
1034
+ // 100 elements, and the page's budget holds them.
1035
+ pageObjectIds: pageIds.length > 0 ? pageIds : undefined,
1036
+ objectIds: objectIds.length > 0 ? objectIds : undefined,
1037
+ replyCount: replies.length,
1038
+ idsNotShown: (text(thread["commentId"]) !== undefined && commentId === undefined) ||
1039
+ (text(thread["anchorId"]) !== undefined && anchorId === undefined) ||
1040
+ head?.idsNotShown ||
1041
+ replies.some((reply) => reply.idsNotShown) ||
1042
+ undefined,
1043
+ }),
1044
+ quote: typeof thread["plainTextQuote"] === "string" ? redactBeforeCut(ctx, thread["plainTextQuote"]) : "",
1045
+ head,
1046
+ replies,
1047
+ };
1048
+ }
1049
+ /**
1050
+ * A post row carrying `content` from `offset`, and, when cut — which also
1051
+ * marks its thread `truncated` — the whole length in `contentLength`.
1052
+ */
1053
+ function postRow(post, content, offset, cutFrom) {
1054
+ return compact({
1055
+ ...post.row,
1056
+ content: content === "" && cutFrom === undefined ? undefined : content,
1057
+ contentOffset: offset > 0 ? offset : undefined,
1058
+ contentLength: cutFrom,
1059
+ });
1060
+ }
1061
+ /**
1062
+ * Part `j` of a thread placed into its row with `content` from `offset`:
1063
+ * 0 the quote, 1 the head post, 2 on each reply in turn. `undefined` for a
1064
+ * part the thread does not have. `cutFrom`, the part's whole length, marks it
1065
+ * cut.
1066
+ */
1067
+ function placePart(thread, row, j, content, offset, cutFrom) {
1068
+ if (j === 0) {
1069
+ if (thread.quote === "")
1070
+ return undefined;
1071
+ return compact({ ...row, quote: content, quoteOffset: offset > 0 ? offset : undefined, quoteLength: cutFrom });
1072
+ }
1073
+ if (j === 1)
1074
+ return thread.head ? { ...row, headPost: postRow(thread.head, content, offset, cutFrom) } : undefined;
1075
+ const reply = thread.replies[j - 2];
1076
+ if (!reply)
1077
+ return undefined;
1078
+ const replies = asArray(row["replies"]);
1079
+ return compact({
1080
+ ...row,
1081
+ // A row that continues a thread from a previous page starts its replies past 0.
1082
+ repliesOffset: replies.length === 0 && j > 2 ? j - 2 : row["repliesOffset"],
1083
+ replies: [...replies, postRow(reply, content, offset, cutFrom)],
1084
+ });
1085
+ }
1086
+ function partText(thread, j) {
1087
+ if (j === 0)
1088
+ return thread.quote;
1089
+ if (j === 1)
1090
+ return thread.head?.content ?? "";
1091
+ return thread.replies[j - 2]?.content ?? "";
1092
+ }
1093
+ /**
1094
+ * Thread rows from thread `i`, part `j`, text offset `o`, under `limit` rows
1095
+ * and `room` bytes. Every thread's ids come whole on every row it appears
1096
+ * on; a quote or post too long for what is left is cut at a character, and
1097
+ * the next page continues that thread (`continued`) from the cut — the same
1098
+ * way get_page continues an element's text.
1099
+ */
1100
+ function threadRows(threads, start, limit, room) {
1101
+ const out = [];
1102
+ let used = 2;
1103
+ let { i, j, o } = start;
1104
+ const fits = (row) => used + jsonBytes(row) + 1 <= room;
1105
+ const unfit = () => new ConnectorCallError("connector_call_failed", "list_comments could not fit any of a comment thread in one result; slides_api_read pages it in JSON chunks.", {
1106
+ retryable: false,
1107
+ });
1108
+ while (i < threads.length && out.length < limit) {
1109
+ const thread = threads[i];
1110
+ // Where this row starts: a row that places nothing leaves the next page
1111
+ // to start here, so it is not called continued for parts it skipped.
1112
+ const from = { i, j, o };
1113
+ let row = compact({ ...thread.row, continued: j > 0 || o > 0 ? true : undefined });
1114
+ if (!fits(row)) {
1115
+ if (out.length > 0)
1116
+ break;
1117
+ throw unfit();
1118
+ }
1119
+ let placed = false;
1120
+ for (const parts = thread.replies.length + 2; j < parts; j += 1, o = 0) {
1121
+ const whole = partText(thread, j);
1122
+ const rest = whole.slice(o);
1123
+ const next = placePart(thread, row, j, rest, o);
1124
+ if (next === undefined)
1125
+ continue;
1126
+ if (fits(next)) {
1127
+ row = next;
1128
+ placed = true;
1129
+ continue;
1130
+ }
1131
+ // The row as it would be with this part cut, so the text's share is
1132
+ // measured against everything else the row carries.
1133
+ const shell = { ...placePart(thread, row, j, "", o, whole.length), truncated: true };
1134
+ const available = room - used - 1 - (jsonBytes(shell) - 2);
1135
+ const cut = available > 2 && rest !== ""
1136
+ ? prefixWithin(rest, available, (dropped) => `\n[… ${dropped} more characters continue on the next page]`)
1137
+ : { text: "", kept: 0 };
1138
+ if (cut.kept === 0) {
1139
+ if (!placed) {
1140
+ if (out.length > 0)
1141
+ return { threads: out, next: from };
1142
+ throw unfit();
1143
+ }
1144
+ out.push({ ...row, truncated: true });
1145
+ return { threads: out, next: { i, j, o } };
1146
+ }
1147
+ out.push({ ...placePart(thread, row, j, cut.text, o, whole.length), truncated: true });
1148
+ return { threads: out, next: { i, j, o: o + cut.kept } };
1149
+ }
1150
+ out.push(row);
1151
+ used += jsonBytes(row) + 1;
1152
+ i += 1;
1153
+ j = 0;
1154
+ o = 0;
1155
+ }
1156
+ return { threads: out, next: i < threads.length ? { i, j, o } : undefined };
1157
+ }
1158
+ /**
1159
+ * The state a list_comments cursor binds to: the revision, the threads
1160
+ * themselves, and the anchors that place them. Slides saves comments apart
1161
+ * from the deck, so a reply added between pages need not move the revision,
1162
+ * and an anchor moved from one element to another need not change a thread;
1163
+ * binding to the exact JSON of both makes either a `conflict`, never a page
1164
+ * that skips, repeats, or places a thread where it no longer is.
1165
+ */
1166
+ function commentsState(ctx, revisionId, comments, pages) {
1167
+ return stateOf(ctx, undefined, () => [
1168
+ text(revisionId) ?? null,
1169
+ comments,
1170
+ pages.map((page) => [page["objectId"] ?? null, page["commentAnchors"] ?? null]),
1171
+ ]);
1172
+ }
1173
+ /** A 403 on a comment read: what it most likely means, beside Google's words. */
1174
+ function commentAccess(connection, failure) {
1175
+ return googleOutcomeOf(failure)?.status === 403
1176
+ ? googleFailureMessage(connection, failure, (message) => `${message} Reading comments needs comment access: an account that may only view the deck cannot read them.`)
1177
+ : Effect.fail(failure);
1178
+ }
1179
+ /** A comment's text, refused locally past Slides' own cap rather than sent to fail. */
1180
+ function commentText(value, field) {
1181
+ if (value === undefined)
1182
+ return undefined;
1183
+ const content = String(value);
1184
+ const bytes = new TextEncoder().encode(content).length;
1185
+ if (bytes > MAX_COMMENT_BYTES) {
1186
+ throw new ConnectorCallError("invalid_args", `${field} is ${bytes} UTF-8 bytes; Slides accepts at most ${MAX_COMMENT_BYTES}. Nothing was sent.`);
1187
+ }
1188
+ return content;
1189
+ }
1190
+ /**
1191
+ * One comment request as its own batch, and what its result always carries:
1192
+ * the deck, the revision it left, and — whenever Slides does not confirm the
1193
+ * comment saved — the state it reported and what to do about it.
1194
+ */
1195
+ function commentWrite(client, ctx, presentationId, request, requiredRevisionId) {
1196
+ return Effect.gen(function* () {
1197
+ const response = yield* batchUpdate(client, presentationId, [request], requiredRevisionId);
1198
+ const state = commentSaveFields(ctx, response, true);
1199
+ return {
1200
+ reply: asRecord(asArray(response["replies"])[0]),
1201
+ header: compact({ presentationId, ...revisionFields(response), ...state }),
1202
+ saved: state.commentUpdateState === undefined,
1203
+ };
1204
+ });
1205
+ }
1206
+ /**
1207
+ * One post added to a thread — a reply, or a status or assignee change —
1208
+ * and its result: the new post's id and author whole, or the write's comment
1209
+ * state when Slides does not confirm it saved.
1210
+ */
1211
+ function postToThread(connection, client, ctx, tool, args, post) {
1212
+ return Effect.gen(function* () {
1213
+ const written = yield* commentWrite(client, ctx, args["presentationId"], {
1214
+ addCommentReply: { commentId: args["commentId"], post: compact(post) },
1215
+ }).pipe(Effect.catch((failure) => afterUnknownCreate(connection, failure, "Read the thread with list_comments and look for the post before sending it again; a second one is a second post.")));
1216
+ const reread = "read the thread with list_comments to find the post";
1217
+ const created = asRecord(written.reply["addCommentReply"])["post"];
1218
+ if (wholeId(asRecord(created)["postId"]) === undefined && written.saved) {
1219
+ throw appliedButUnreadable(tool, "Google returned no usable id for the new post", reread);
1220
+ }
1221
+ return appliedDeliverable(compact({
1222
+ ...written.header,
1223
+ commentId: args["commentId"],
1224
+ post: created && typeof created === "object" ? writtenPost(ctx, created) : undefined,
1225
+ idsNotShown: postPart(ctx, created).idsNotShown || undefined,
1226
+ note: written.saved ? undefined : COMMENTS_UNSAVED,
1227
+ }), tool, reread);
1228
+ });
1229
+ }
1230
+ export function slidesTools({ connection }) {
1231
+ const client = googleClient(connection, SLIDES_INDEX);
1232
+ const readOnly = { readOnlyHint: true };
1233
+ const presentationPath = (id) => `/presentations/${encodeURIComponent(id)}`;
1234
+ const nextPage = (cursor) => cursor === null ? { hasMore: false } : { hasMore: true, nextCursor: cursor };
1235
+ const declarations = [
1236
+ {
1237
+ name: "get_presentation",
1238
+ description: "Read a Google Slides deck: title, page size, revisionId, layouts, and each slide's text in reading order with speaker notes, capped per slide. Cannot find decks; that is Drive's job.",
1239
+ annotations: readOnly,
1240
+ input: contracts.get_presentation.input,
1241
+ output: contracts.get_presentation.output,
1242
+ run: (args, ctx) => Effect.gen(function* () {
1243
+ const presentationId = args["presentationId"];
1244
+ const cursor = decodeCursor(args["cursor"], { k: "deck", p: presentationId });
1245
+ const presentation = asRecord(yield* client.json({
1246
+ method: "GET",
1247
+ template: "/presentations/{presentationsId}",
1248
+ path: presentationPath(presentationId),
1249
+ query: { fields: PRESENTATION_FIELDS },
1250
+ }));
1251
+ const all = asArray(presentation["slides"]);
1252
+ const revisionId = presentation["revisionId"];
1253
+ const state = yield* Effect.promise(() => stateOf(ctx, revisionId, () => all.map((slide) => asRecord(slide)["objectId"] ?? null)));
1254
+ assertUnchanged(cursor, state, "deck's slides");
1255
+ const start = cursor?.i ?? 0;
1256
+ const limit = typeof args["limit"] === "number" ? args["limit"] : DEFAULT_PAGE_SIZE;
1257
+ const maxChars = typeof args["maxCharsPerSlide"] === "number" ? args["maxCharsPerSlide"] : DEFAULT_SLIDE_CHARS;
1258
+ const deck = { presentationId, revisionId, layouts: layoutNames(ctx, presentation) };
1259
+ const pageSize = asRecord(presentation["pageSize"]);
1260
+ const width = dimension(pageSize["width"]);
1261
+ const height = dimension(pageSize["height"]);
1262
+ // The first projected page previews the layouts, bounded; the rest
1263
+ // continue in list_layouts from layoutsCursor.
1264
+ let preview = {};
1265
+ if (start === 0) {
1266
+ const rows = layoutRows(ctx, presentation);
1267
+ const shown = rowsWithin(rows, 0, MAX_ROWS, MAX_LAYOUT_PREVIEW_BYTES);
1268
+ preview = compact({
1269
+ layouts: shown.rows,
1270
+ layoutsNotShown: shown.next < rows.length ? rows.length - shown.next : undefined,
1271
+ layoutsCursor: shown.next < rows.length
1272
+ ? encodeCursor(ctx, {
1273
+ k: "layouts",
1274
+ p: presentationId,
1275
+ s: yield* Effect.promise(() => stateOf(ctx, revisionId, () => layoutFingerprint(rows))),
1276
+ i: shown.next,
1277
+ })
1278
+ : undefined,
1279
+ });
1280
+ }
1281
+ const header = compact({
1282
+ presentationId: text(presentation["presentationId"]) ?? presentationId,
1283
+ title: label(ctx, text(presentation["title"]), MAX_TITLE_BYTES, "Drive shows the whole title"),
1284
+ // Only an account that may edit the deck is given one.
1285
+ revisionId: text(revisionId),
1286
+ url: editUrl(presentationId),
1287
+ locale: text(presentation["locale"]),
1288
+ canvasSize: width && height ? { width, height } : undefined,
1289
+ slideCount: all.length,
1290
+ // The one page no slide, layout, or master links to; get_page reads it.
1291
+ notesMasterId: text(asRecord(presentation["notesMaster"])["objectId"]),
1292
+ ...preview,
1293
+ });
1294
+ const issue = (i) => encodeCursor(ctx, { k: "deck", p: presentationId, s: state, i });
1295
+ // A page ends at the slide limit or at the byte budget, whichever is
1296
+ // first; the budget counts the header and the longest cursor.
1297
+ let used = jsonBytes({ ...header, slides: [] }) + pageEnvelopeBytes({ k: "deck", p: presentationId, s: state, i: 0 });
1298
+ const slides = [];
1299
+ let next = start;
1300
+ while (next < all.length && slides.length < limit) {
1301
+ const room = RESULT_BUDGET_BYTES - used - 1;
1302
+ let slide = finished(yield* Effect.promise(() => projectSlide(ctx, all[next], next, deck, maxChars)));
1303
+ let size = jsonBytes(slide);
1304
+ if (size > room) {
1305
+ if (slides.length > 0)
1306
+ break;
1307
+ slide = yield* Effect.promise(() => fitSlide(ctx, all[next], next, deck, maxChars, room));
1308
+ size = jsonBytes(slide);
1309
+ }
1310
+ slides.push(slide);
1311
+ used += size + 1;
1312
+ next += 1;
1313
+ }
1314
+ return deliverable({ ...header, slides, page: nextPage(next < all.length ? issue(next) : null) }, "get_presentation");
1315
+ }),
1316
+ },
1317
+ {
1318
+ name: "get_page",
1319
+ description: "Read one page by objectId — slide, layout, master, or notes page — every element in reading order, with placeholder ids and text continued across pages.",
1320
+ annotations: readOnly,
1321
+ input: contracts.get_page.input,
1322
+ output: contracts.get_page.output,
1323
+ run: (args, ctx) => Effect.gen(function* () {
1324
+ const presentationId = args["presentationId"];
1325
+ const pageObjectId = args["pageObjectId"];
1326
+ const cursor = decodeCursor(args["cursor"], { k: "page", p: presentationId, g: pageObjectId });
1327
+ const page = asRecord(yield* client.json({
1328
+ method: "GET",
1329
+ template: "/presentations/{presentationId}/pages/{pageObjectId}",
1330
+ path: `${presentationPath(presentationId)}/pages/${encodeURIComponent(pageObjectId)}`,
1331
+ query: { fields: PAGE_FIELDS },
1332
+ }));
1333
+ const leaves = readingOrder(ctx, page["pageElements"]);
1334
+ const state = yield* Effect.promise(() => stateOf(ctx, page["revisionId"], () => leafFingerprint(leaves), true));
1335
+ assertUnchanged(cursor, state, "page");
1336
+ const header = compact({
1337
+ presentationId,
1338
+ pageObjectId: text(page["objectId"]) ?? pageObjectId,
1339
+ revisionId: text(page["revisionId"]),
1340
+ ...describePage(ctx, page),
1341
+ elementCount: leaves.length,
1342
+ });
1343
+ const room = RESULT_BUDGET_BYTES -
1344
+ jsonBytes({ ...header, elements: [] }) -
1345
+ pageEnvelopeBytes({
1346
+ k: "page",
1347
+ p: presentationId,
1348
+ g: pageObjectId,
1349
+ s: state,
1350
+ i: 0,
1351
+ }) -
1352
+ 64;
1353
+ const limit = typeof args["limit"] === "number" ? args["limit"] : DEFAULT_ELEMENTS;
1354
+ const start = { i: cursor?.i ?? 0, o: cursor?.o ?? 0 };
1355
+ const rows = projectedRows(ctx, leaves, start, limit, room);
1356
+ const nextCursor = rows.next
1357
+ ? encodeCursor(ctx, {
1358
+ k: "page",
1359
+ p: presentationId,
1360
+ g: pageObjectId,
1361
+ s: state,
1362
+ i: rows.next.i,
1363
+ o: rows.next.o ? rows.next.o : undefined,
1364
+ })
1365
+ : null;
1366
+ return deliverable(compact({
1367
+ ...header,
1368
+ properties: rows.properties,
1369
+ propertiesJson: rows.propertiesJson,
1370
+ elements: rows.elements,
1371
+ page: nextPage(nextCursor),
1372
+ }), "get_page");
1373
+ }),
1374
+ },
1375
+ {
1376
+ name: "list_layouts",
1377
+ description: "List a deck's masters, each followed by its layouts, with the ids create_slide and get_page take. Names only; get_page reads a layout's placeholders.",
1378
+ annotations: readOnly,
1379
+ input: contracts.list_layouts.input,
1380
+ output: contracts.list_layouts.output,
1381
+ run: (args, ctx) => Effect.gen(function* () {
1382
+ const presentationId = args["presentationId"];
1383
+ const cursor = decodeCursor(args["cursor"], { k: "layouts", p: presentationId });
1384
+ const presentation = asRecord(yield* client.json({
1385
+ method: "GET",
1386
+ template: "/presentations/{presentationsId}",
1387
+ path: presentationPath(presentationId),
1388
+ query: { fields: LAYOUT_LIST_FIELDS },
1389
+ }));
1390
+ const rows = layoutRows(ctx, presentation);
1391
+ const state = yield* Effect.promise(() => stateOf(ctx, presentation["revisionId"], () => layoutFingerprint(rows)));
1392
+ assertUnchanged(cursor, state, "deck's layouts");
1393
+ const header = compact({
1394
+ presentationId,
1395
+ revisionId: text(presentation["revisionId"]),
1396
+ notesMasterId: text(asRecord(presentation["notesMaster"])["objectId"]),
1397
+ total: rows.length,
1398
+ });
1399
+ const room = RESULT_BUDGET_BYTES -
1400
+ jsonBytes({ ...header, layouts: [] }) -
1401
+ pageEnvelopeBytes({ k: "layouts", p: presentationId, s: state, i: 0 });
1402
+ const limit = typeof args["limit"] === "number" ? args["limit"] : DEFAULT_LAYOUTS;
1403
+ const shown = rowsWithin(rows, cursor?.i ?? 0, limit, room);
1404
+ const next = shown.next < rows.length
1405
+ ? encodeCursor(ctx, { k: "layouts", p: presentationId, s: state, i: shown.next })
1406
+ : null;
1407
+ return deliverable({ ...header, layouts: shown.rows, page: nextPage(next) }, "list_layouts");
1408
+ }),
1409
+ },
1410
+ {
1411
+ name: "list_comments",
1412
+ description: "List a deck's comment threads, or one page's: anchor, status, quoted text, head post, and replies, each author by user id. Long text continues across pages.",
1413
+ annotations: readOnly,
1414
+ input: contracts.list_comments.input,
1415
+ output: contracts.list_comments.output,
1416
+ run: (args, ctx) => Effect.gen(function* () {
1417
+ const presentationId = args["presentationId"];
1418
+ const pageObjectId = args["pageObjectId"];
1419
+ const scope = pageObjectId === undefined ? {} : { g: pageObjectId };
1420
+ const cursor = decodeCursor(args["cursor"], { k: "comments", p: presentationId, ...scope });
1421
+ const source = asRecord(yield* client
1422
+ .json({
1423
+ method: "GET",
1424
+ template: pageObjectId === undefined
1425
+ ? "/presentations/{presentationsId}"
1426
+ : "/presentations/{presentationId}/pages/{pageObjectId}",
1427
+ path: pageObjectId === undefined
1428
+ ? presentationPath(presentationId)
1429
+ : `${presentationPath(presentationId)}/pages/${encodeURIComponent(pageObjectId)}`,
1430
+ query: {
1431
+ commentsViewMode: COMMENTS_INCLUDED,
1432
+ fields: pageObjectId === undefined ? COMMENT_DECK_FIELDS : COMMENT_PAGE_FIELDS,
1433
+ },
1434
+ })
1435
+ .pipe(Effect.catch((failure) => commentAccess(connection, failure))));
1436
+ // Asked for INCLUDED, Slides either includes every thread or refuses;
1437
+ // a reply in any other mode could be missing threads, so it is not
1438
+ // passed off as the list.
1439
+ const mode = text(source["commentsViewMode"]);
1440
+ if (mode !== undefined && mode !== COMMENTS_INCLUDED) {
1441
+ throw new ConnectorCallError("connector_call_failed", `Slides answered in comments view mode ${label(ctx, mode, MAX_NAME_BYTES, "")}, not ${COMMENTS_INCLUDED}, so its threads may be incomplete. It only read, so nothing was changed.`, { retryable: false });
1442
+ }
1443
+ const comments = redactBeforeCut(ctx, asArray(source["comments"]));
1444
+ const pages = pageObjectId === undefined ? commentPages(source) : [source];
1445
+ const state = yield* Effect.promise(() => commentsState(ctx, source["revisionId"], comments, pages));
1446
+ assertUnchanged(cursor, state, pageObjectId === undefined ? "deck's comments" : "page's comments");
1447
+ const header = compact({
1448
+ presentationId,
1449
+ pageObjectId,
1450
+ revisionId: text(source["revisionId"]),
1451
+ total: comments.length,
1452
+ });
1453
+ const room = RESULT_BUDGET_BYTES -
1454
+ jsonBytes({ ...header, threads: [] }) -
1455
+ pageEnvelopeBytes({
1456
+ k: "comments",
1457
+ p: presentationId,
1458
+ ...scope,
1459
+ s: state,
1460
+ i: 0,
1461
+ j: 0,
1462
+ }) -
1463
+ 64;
1464
+ const limit = typeof args["limit"] === "number" ? args["limit"] : DEFAULT_THREADS;
1465
+ let shown;
1466
+ const places = anchorPlaces(pages);
1467
+ const threads = comments.map((thread) => threadParts(ctx, thread, places));
1468
+ shown = threadRows(threads, { i: cursor?.i ?? 0, j: cursor?.j ?? 0, o: cursor?.o ?? 0 }, limit, room);
1469
+ const next = shown.next
1470
+ ? encodeCursor(ctx, {
1471
+ k: "comments",
1472
+ p: presentationId,
1473
+ ...scope,
1474
+ s: state,
1475
+ i: shown.next.i,
1476
+ j: shown.next.j ? shown.next.j : undefined,
1477
+ o: shown.next.o ? shown.next.o : undefined,
1478
+ })
1479
+ : null;
1480
+ return deliverable({ ...header, threads: shown.threads, page: nextPage(next) }, "list_comments", "pass a smaller limit, or one page's pageObjectId");
1481
+ }),
1482
+ },
1483
+ {
1484
+ name: "get_slide_thumbnail",
1485
+ description: "Get a PNG thumbnail link for one slide: a URL valid about 30 minutes that opens as the user, plus its size. Never downloads the image.",
1486
+ annotations: readOnly,
1487
+ input: contracts.get_slide_thumbnail.input,
1488
+ output: contracts.get_slide_thumbnail.output,
1489
+ run: (args) => Effect.gen(function* () {
1490
+ const thumbnail = asRecord(yield* client.json({
1491
+ method: "GET",
1492
+ template: "/presentations/{presentationId}/pages/{pageObjectId}/thumbnail",
1493
+ path: `/presentations/${encodeURIComponent(args["presentationId"])}/pages/${encodeURIComponent(args["slideObjectId"])}/thumbnail`,
1494
+ query: {
1495
+ "thumbnailProperties.mimeType": "PNG",
1496
+ "thumbnailProperties.thumbnailSize": args["size"],
1497
+ },
1498
+ }));
1499
+ const contentUrl = text(thumbnail["contentUrl"]) ?? "";
1500
+ // A link is whole or useless, so one too long to pass on is refused,
1501
+ // never cut.
1502
+ if (jsonBytes(contentUrl) > MAX_URL_BYTES) {
1503
+ throw new ConnectorCallError("connector_call_failed", `Google returned a thumbnail link of ${jsonBytes(contentUrl)} bytes, longer than this connection passes on (${MAX_URL_BYTES}). It only read, so nothing was changed; open the slide in Slides instead.`, { retryable: false });
1504
+ }
1505
+ return deliverable(compact({
1506
+ contentUrl,
1507
+ width: typeof thumbnail["width"] === "number" ? thumbnail["width"] : undefined,
1508
+ height: typeof thumbnail["height"] === "number" ? thumbnail["height"] : undefined,
1509
+ }), "get_slide_thumbnail", "open the slide in Slides instead");
1510
+ }),
1511
+ },
1512
+ {
1513
+ name: "create_presentation",
1514
+ description: "Create an empty Google Slides deck with one title slide in the user's My Drive root. It cannot place the deck in a folder or copy a template.",
1515
+ // Additive: a new file changes nothing that existed. Not read-only, so
1516
+ // program calls require a writable endpoint; other writes use
1517
+ // call_destructive_tool. The host controls approval.
1518
+ annotations: { readOnlyHint: false, destructiveHint: false },
1519
+ input: contracts.create_presentation.input,
1520
+ output: contracts.create_presentation.output,
1521
+ run: (args, ctx) => Effect.gen(function* () {
1522
+ const presentation = asRecord(yield* client
1523
+ .json({
1524
+ method: "POST",
1525
+ template: "/presentations",
1526
+ path: "/presentations",
1527
+ body: { title: args["title"] },
1528
+ })
1529
+ .pipe(Effect.catch((failure) => afterUnknownCreate(connection, failure, "Search Drive for a deck with this title before creating it again; a second create makes a second deck."))));
1530
+ const lookInDrive = "search Drive for the deck by its title";
1531
+ const id = wholeId(presentation["presentationId"]);
1532
+ if (id === undefined) {
1533
+ throw appliedButUnreadable("create_presentation", "Google returned no usable id for the new deck", lookInDrive);
1534
+ }
1535
+ const revisionId = presentation["revisionId"];
1536
+ const slideIds = asArray(presentation["slides"]).map((slide) => asRecord(slide)["objectId"]);
1537
+ const shownIds = slideIds.map(wholeId).filter((value) => value !== undefined);
1538
+ return appliedDeliverable(compact({
1539
+ presentationId: id,
1540
+ revisionId: wholeId(revisionId),
1541
+ revisionIdNotShown: revisionId !== undefined && wholeId(revisionId) === undefined ? true : undefined,
1542
+ title: label(ctx, text(presentation["title"]), MAX_TITLE_BYTES, "Drive shows the whole title"),
1543
+ url: editUrl(id),
1544
+ slideObjectIds: shownIds,
1545
+ slideObjectIdsNotShown: shownIds.length < slideIds.length ? slideIds.length - shownIds.length : undefined,
1546
+ }), "create_presentation", lookInDrive);
1547
+ }),
1548
+ },
1549
+ {
1550
+ name: "create_slide",
1551
+ description: "Add one empty slide to a deck, from a predefined layout or one of its own layouts, at an index or the end. Adds no text; nothing existing changes.",
1552
+ // Additive: later slides move down one place, and nothing is lost.
1553
+ annotations: { readOnlyHint: false, destructiveHint: false },
1554
+ input: contracts.create_slide.input,
1555
+ output: contracts.create_slide.output,
1556
+ run: (args) => Effect.gen(function* () {
1557
+ if (args["layout"] !== undefined && args["layoutId"] !== undefined) {
1558
+ throw new ConnectorCallError("invalid_args", "Pass layout or layoutId, not both.");
1559
+ }
1560
+ const reference = args["layoutId"] !== undefined
1561
+ ? { layoutId: args["layoutId"] }
1562
+ : args["layout"] !== undefined
1563
+ ? { predefinedLayout: args["layout"] }
1564
+ : undefined;
1565
+ const response = yield* batchUpdate(client, args["presentationId"], [
1566
+ { createSlide: compact({ insertionIndex: args["insertionIndex"], slideLayoutReference: reference }) },
1567
+ ]).pipe(Effect.catch((failure) => afterUnknownCreate(connection, failure, "Read the deck with get_presentation and look for the new slide before adding another.")));
1568
+ const reread = "read the deck with get_presentation to find the new slide";
1569
+ const slideObjectId = wholeId(asRecord(asRecord(asArray(response["replies"])[0])["createSlide"])["objectId"]);
1570
+ if (slideObjectId === undefined) {
1571
+ throw appliedButUnreadable("create_slide", "Google returned no usable id for the new slide", reread);
1572
+ }
1573
+ return appliedDeliverable(compact({ presentationId: args["presentationId"], ...revisionFields(response), slideObjectId }), "create_slide", reread);
1574
+ }),
1575
+ },
1576
+ {
1577
+ name: "replace_all_text",
1578
+ description: "Replace every occurrence of literal text across a deck's slides, or the slides named, in one atomic write. No regex; formatting of the replaced text is kept.",
1579
+ // Destructive: the replaced text is gone, everywhere it matched.
1580
+ annotations: { readOnlyHint: false, destructiveHint: true },
1581
+ input: contracts.replace_all_text.input,
1582
+ output: contracts.replace_all_text.output,
1583
+ run: (args) => Effect.gen(function* () {
1584
+ const replacements = asArray(args["replacements"]).map(asRecord);
1585
+ const response = yield* batchUpdate(client, args["presentationId"], replacements.map((replacement) => ({
1586
+ replaceAllText: compact({
1587
+ containsText: { text: replacement["find"], matchCase: replacement["matchCase"] !== false },
1588
+ replaceText: replacement["replace"],
1589
+ pageObjectIds: args["slideObjectIds"],
1590
+ }),
1591
+ })), args["requiredRevisionId"]);
1592
+ const replies = asArray(response["replies"]);
1593
+ const counted = replacements.map((replacement, index) => {
1594
+ // Proto3 omits a zero count.
1595
+ const changed = asRecord(asRecord(replies[index])["replaceAllText"])["occurrencesChanged"];
1596
+ // Echoed for reading the counts, not in full: fifty long finds
1597
+ // would make a large result of text the caller already has.
1598
+ const find = String(replacement["find"]);
1599
+ return {
1600
+ find: find.length > MAX_ECHO_CHARS ? `${headOf(find, MAX_ECHO_CHARS)}…` : find,
1601
+ occurrencesChanged: typeof changed === "number" ? changed : 0,
1602
+ };
1603
+ });
1604
+ return appliedDeliverable(compact({
1605
+ presentationId: args["presentationId"],
1606
+ ...revisionFields(response),
1607
+ occurrencesChanged: counted.reduce((sum, entry) => sum + entry.occurrencesChanged, 0),
1608
+ replacements: counted,
1609
+ }), "replace_all_text", "read the deck with get_presentation to see the replaced text");
1610
+ }),
1611
+ },
1612
+ {
1613
+ name: "create_comment",
1614
+ description: "Comment on a slide, a notes page, an element, a range of its text, or a table cell, optionally assigning it. Adds a thread; Slides notifies as in the editor.",
1615
+ // Additive: a new thread changes nothing that existed. Not read-only,
1616
+ // so program calls require a writable endpoint; other writes use
1617
+ // call_destructive_tool. The host controls approval.
1618
+ annotations: { readOnlyHint: false, destructiveHint: false },
1619
+ input: contracts.create_comment.input,
1620
+ output: contracts.create_comment.output,
1621
+ run: (args, ctx) => Effect.gen(function* () {
1622
+ const content = commentText(args["content"], "content");
1623
+ const objectId = args["objectId"];
1624
+ const range = args["textRange"] === undefined ? undefined : asRecord(args["textRange"]);
1625
+ if (range && Number(range["endIndex"]) <= Number(range["startIndex"])) {
1626
+ throw new ConnectorCallError("invalid_args", "textRange.endIndex must be greater than startIndex. Nothing was sent.");
1627
+ }
1628
+ const textRange = range && {
1629
+ type: "FIXED_RANGE",
1630
+ startIndex: range["startIndex"],
1631
+ endIndex: range["endIndex"],
1632
+ };
1633
+ const cell = args["cell"] === undefined ? undefined : asRecord(args["cell"]);
1634
+ const location = cell && { rowIndex: cell["rowIndex"], columnIndex: cell["columnIndex"] };
1635
+ // One anchor: the object itself, text in a shape, text in a cell, or a whole cell.
1636
+ const anchor = location && textRange
1637
+ ? { tableCellTextAnchor: { objectId, cellLocation: location, textRange } }
1638
+ : location
1639
+ ? { tableAnchor: { objectId, tableRange: { location, rowSpan: 1, columnSpan: 1 } } }
1640
+ : textRange
1641
+ ? { shapeTextAnchor: { objectId, textRange } }
1642
+ : { objectId };
1643
+ const written = yield* commentWrite(client, ctx, args["presentationId"], { insertComment: compact({ ...anchor, content, assigneeEmailAddress: args["assigneeEmail"] }) }, args["requiredRevisionId"]).pipe(Effect.catch((failure) => afterUnknownCreate(connection, failure, "List the deck's comments with list_comments and look for it before commenting again; a second insert makes a second thread.")));
1644
+ const reread = "list the deck's comments with list_comments to find it";
1645
+ const thread = asRecord(asRecord(written.reply["insertComment"])["commentThread"]);
1646
+ const commentId = wholeId(thread["commentId"]);
1647
+ if (commentId === undefined && written.saved) {
1648
+ throw appliedButUnreadable("create_comment", "Google returned no usable id for the new comment", reread);
1649
+ }
1650
+ const parts = threadParts(ctx, thread, new Map());
1651
+ const replies = asArray(thread["replies"]);
1652
+ return appliedDeliverable(compact({
1653
+ ...written.header,
1654
+ commentId,
1655
+ anchorId: wholeId(thread["anchorId"]),
1656
+ status: parts.row["status"],
1657
+ quote: label(ctx, text(parts.quote), MAX_POST_BYTES, "list_comments has the whole quote"),
1658
+ headPost: thread["headPost"] ? writtenPost(ctx, thread["headPost"]) : undefined,
1659
+ // A new thread has none; any Slides sends keep their ids.
1660
+ replies: replies.length > 0 ? replies.map((reply) => writtenPost(ctx, reply, MAX_NAME_BYTES)) : undefined,
1661
+ idsNotShown: parts.row["idsNotShown"],
1662
+ note: written.saved ? undefined : COMMENTS_UNSAVED,
1663
+ }), "create_comment", reread);
1664
+ }),
1665
+ },
1666
+ {
1667
+ name: "create_comment_reply",
1668
+ description: "Reply to a comment thread. Adds a post and changes no existing one; to resolve, reopen, or reassign, use update_comment_thread.",
1669
+ // Additive: a reply is a new post, and the thread's status and
1670
+ // assignee stay as they were.
1671
+ annotations: { readOnlyHint: false, destructiveHint: false },
1672
+ input: contracts.create_comment_reply.input,
1673
+ output: contracts.create_comment_reply.output,
1674
+ run: (args, ctx) => postToThread(connection, client, ctx, "create_comment_reply", args, {
1675
+ content: commentText(args["content"], "content"),
1676
+ }),
1677
+ },
1678
+ {
1679
+ name: "update_comment_thread",
1680
+ description: "Resolve, reopen, or reassign a comment thread with a post. Pass exactly one of status or assigneeEmail; reassignment requires content. Replaces its status or assignee; Slides notifies an assignee.",
1681
+ // Destructive: the thread's status or assignee is replaced. The post
1682
+ // that records it cannot be deleted afterwards.
1683
+ annotations: { readOnlyHint: false, destructiveHint: true },
1684
+ // Keep the field schema flat: discovery renders oneOf before properties,
1685
+ // losing these fields for required-only branches. The handler enforces
1686
+ // exactly one change and text on reassignment before dispatch instead.
1687
+ input: contracts.update_comment_thread.input,
1688
+ output: contracts.update_comment_thread.output,
1689
+ run: (args, ctx) => Effect.gen(function* () {
1690
+ // Exactly one change per post. Slides requires text on reassignment;
1691
+ // only RESOLVE and REOPEN may go without.
1692
+ if ((args["status"] === undefined) === (args["assigneeEmail"] === undefined)) {
1693
+ throw new ConnectorCallError("invalid_args", "Pass exactly one of status and assigneeEmail. Nothing was sent.");
1694
+ }
1695
+ if (args["assigneeEmail"] !== undefined && args["content"] === undefined) {
1696
+ throw new ConnectorCallError("invalid_args", "A reassignment needs content: Slides requires text on any post that does not resolve or reopen. Nothing was sent.");
1697
+ }
1698
+ return yield* postToThread(connection, client, ctx, "update_comment_thread", args, {
1699
+ content: commentText(args["content"], "content"),
1700
+ commentAction: args["status"] === undefined ? undefined : args["status"] === "RESOLVED" ? "RESOLVE" : "REOPEN",
1701
+ assigneeEmail: args["assigneeEmail"],
1702
+ });
1703
+ }),
1704
+ },
1705
+ {
1706
+ name: "update_comment_post",
1707
+ description: "Replace the text of one post in a comment thread, the head post or a reply. Only its author can; the old text is gone.",
1708
+ // Destructive: the post's previous text is overwritten.
1709
+ annotations: { readOnlyHint: false, destructiveHint: true },
1710
+ input: contracts.update_comment_post.input,
1711
+ output: contracts.update_comment_post.output,
1712
+ run: (args, ctx) => Effect.gen(function* () {
1713
+ const content = commentText(args["content"], "content");
1714
+ const written = yield* commentWrite(client, ctx, args["presentationId"], {
1715
+ updateCommentPost: { commentId: args["commentId"], postId: args["postId"], content },
1716
+ });
1717
+ return appliedDeliverable(compact({
1718
+ ...written.header,
1719
+ commentId: args["commentId"],
1720
+ postId: args["postId"],
1721
+ note: written.saved ? undefined : COMMENTS_UNSAVED,
1722
+ }), "update_comment_post", "read the thread with list_comments to see the post");
1723
+ }),
1724
+ },
1725
+ {
1726
+ name: "delete_comment",
1727
+ description: "Delete a whole comment thread, every reply with it. Only the author of its head post can, and the API cannot restore it.",
1728
+ // Destructive: the thread and its replies are gone.
1729
+ annotations: { readOnlyHint: false, destructiveHint: true },
1730
+ input: contracts.delete_comment.input,
1731
+ output: contracts.delete_comment.output,
1732
+ run: (args, ctx) => Effect.gen(function* () {
1733
+ const written = yield* commentWrite(client, ctx, args["presentationId"], {
1734
+ deleteComment: { commentId: args["commentId"] },
1735
+ });
1736
+ return appliedDeliverable(compact({
1737
+ ...written.header,
1738
+ commentId: args["commentId"],
1739
+ note: written.saved ? undefined : COMMENTS_UNSAVED,
1740
+ }), "delete_comment", "list the deck's comments with list_comments to see whether it is gone");
1741
+ }),
1742
+ },
1743
+ {
1744
+ name: "delete_comment_reply",
1745
+ description: "Delete one reply from a comment thread. Only its author can, and not a reply that resolved, reopened, or assigned the thread.",
1746
+ // Destructive: the reply is gone.
1747
+ annotations: { readOnlyHint: false, destructiveHint: true },
1748
+ input: contracts.delete_comment_reply.input,
1749
+ output: contracts.delete_comment_reply.output,
1750
+ run: (args, ctx) => Effect.gen(function* () {
1751
+ const written = yield* commentWrite(client, ctx, args["presentationId"], {
1752
+ deleteCommentReply: { commentId: args["commentId"], postId: args["postId"] },
1753
+ });
1754
+ return appliedDeliverable(compact({
1755
+ ...written.header,
1756
+ commentId: args["commentId"],
1757
+ postId: args["postId"],
1758
+ note: written.saved ? undefined : COMMENTS_UNSAVED,
1759
+ }), "delete_comment_reply", "read the thread with list_comments to see whether it is gone");
1760
+ }),
1761
+ },
1762
+ {
1763
+ name: "batch_update_presentation",
1764
+ description: "Send Slides batchUpdate at the read revision; insertComment and other comment kinds may share the batch but save apart. Always destructive. Returns every new id; past 256 KiB, only a direct call can page them.",
1765
+ // Destructive: a raw request can delete or overwrite anything in the deck.
1766
+ annotations: { readOnlyHint: false, destructiveHint: true },
1767
+ input: contracts.batch_update_presentation.input,
1768
+ output: contracts.batch_update_presentation.output,
1769
+ run: (args, ctx) => Effect.gen(function* () {
1770
+ const requests = asArray(args["requests"]);
1771
+ const kinds = requests.map((request, index) => [index, Object.keys(asRecord(request))[0] ?? ""]);
1772
+ const unknown = kinds.filter(([, kind]) => !REQUEST_KINDS.includes(kind));
1773
+ if (unknown.length > 0) {
1774
+ throw new ConnectorCallError("invalid_args", `Not a Slides request kind: ${unknown
1775
+ .slice(0, 5)
1776
+ .map(([index, kind]) => `requests[${index}] "${kind.slice(0, 64)}"`)
1777
+ .join(", ")}. Each request is one of Slides' Request kinds, such as insertText, deleteObject, or createShape; nothing was sent.`);
1778
+ }
1779
+ const response = yield* batchUpdate(client, args["presentationId"], requests, args["requiredRevisionId"]);
1780
+ // Comment changes save apart from the rest of a batch, and Slides
1781
+ // reports them on their own: a batch that sent any is settled only
1782
+ // when Slides says ALL_SAVED, and one that is not stops being called
1783
+ // all or none.
1784
+ const saved = commentSaveFields(ctx, response, kinds.some(([, kind]) => COMMENT_KINDS.has(kind)));
1785
+ const header = compact({ presentationId: args["presentationId"], ...revisionFields(response), ...saved });
1786
+ // One reply per request, in order; most are empty, the create replies
1787
+ // carry the new ids. Bounded, ids first: Slides accepted the batch, so
1788
+ // what it created must reach the caller.
1789
+ const comments = kinds.some(([, kind]) => COMMENT_KINDS.has(kind));
1790
+ const reread = `re-read with get_presentation or get_page for deck changes${comments ? ", and list_comments for comments" : ""}`;
1791
+ const all = asArray(response["replies"]);
1792
+ const shown = boundedReplies(ctx, all, jsonBytes(header) + 1024);
1793
+ // Neutral about what applied: the comment state, when unconfirmed,
1794
+ // says that apart; these say only what the result left out.
1795
+ const unsaved = saved.commentUpdateState !== undefined ? COMMENTS_UNSAVED : undefined;
1796
+ const notes = (...entries) => {
1797
+ const said = entries.filter((entry) => entry !== undefined);
1798
+ return said.length > 0 ? `${said.join(" ")} To see the deck as it is now, ${reread}.` : undefined;
1799
+ };
1800
+ const result = compact({
1801
+ ...header,
1802
+ replies: shown.replies,
1803
+ note: notes(unsaved, shown.replies.some((reply) => "cut" in reply)
1804
+ ? `Large reply fields are named in truncated; every id is kept${shown.oversized ? ", and no other text, since the ids alone pass this tool's 192 KiB budget" : ""}.`
1805
+ : undefined),
1806
+ });
1807
+ const bytes = jsonBytes(result);
1808
+ if (bytes <= BRIDGE_BYTES)
1809
+ return result;
1810
+ if (bytes <= MAX_IDS_RESULT_BYTES) {
1811
+ return {
1812
+ ...result,
1813
+ note: `${result.note ?? ""} At ${bytes} bytes this result is more than execute_code can carry (${BRIDGE_BYTES}): call the tool directly and page it with connecta.result.`.trim(),
1814
+ };
1815
+ }
1816
+ // Past any result: the save state and how to recover the ids stay;
1817
+ // the replies do not. Slides accepted the batch, so it is not to be sent again.
1818
+ return compact({
1819
+ ...header,
1820
+ replies: [],
1821
+ repliesNotShown: all.length,
1822
+ note: notes(unsaved, `Slides accepted the batch, but its ${all.length} replies carry ${bytes} bytes of ids, more than any result can carry, so none is shown. Do not send it again; ${comments ? "list_comments has the comments and posts it created, and " : ""}get_presentation or get_page has the objects.`),
1823
+ });
1824
+ }),
1825
+ },
1826
+ ];
1827
+ return declarations.map((declaration) => {
1828
+ const isRead = declaration.annotations.readOnlyHint === true;
1829
+ const isList = declaration.name.startsWith("list_");
1830
+ const rowNames = new Set(Object.keys(declaration.name === "list_layouts" ? contracts.LayoutRow.fields : contracts.CommentRow.fields));
1831
+ const names = isList
1832
+ ? rowNames
1833
+ : new Set(Object.keys(declaration.output.fields));
1834
+ const spec = {
1835
+ name: declaration.name,
1836
+ description: declaration.description,
1837
+ input: isRead
1838
+ ? Schema.Struct({
1839
+ ...declaration.input.fields,
1840
+ fields: Schema.optionalKey(fieldsInput(isList
1841
+ ? "Dot paths to keep in each item; truncation metadata is preserved."
1842
+ : "Dot paths to keep in the result; page and truncation metadata are preserved.")),
1843
+ })
1844
+ : declaration.input,
1845
+ output: declaration.output,
1846
+ ...(declaration.writeSteps ? { writeSteps: declaration.writeSteps } : {}),
1847
+ run: (args) => Effect.gen(function* () {
1848
+ const problem = isRead
1849
+ ? fieldsProblem(args["fields"], names, isList ? "a Slides list item" : "the result")
1850
+ : undefined;
1851
+ if (problem)
1852
+ return yield* Effect.fail(new Refusal({ error: problem }));
1853
+ const current = yield* Effect.serviceOption(Operation);
1854
+ if (Option.isNone(current) || !current.value)
1855
+ return yield* Effect.die(new Error("Slides requires a call context."));
1856
+ const ctx = current.value;
1857
+ const supplied = args["cursor"];
1858
+ const decoded = typeof supplied === "string" ? yield* boundCursor.decode("slides-content-v1", {}, supplied) : undefined;
1859
+ const result = yield* declaration.run({ ...args, ...(decoded === undefined ? {} : { cursor: decoded }) }, ctx);
1860
+ const normalized = normalizeResult(result, declaration.name);
1861
+ const selected = isList
1862
+ ? { ...normalized, items: selectNamedFields(normalized["items"], args["fields"]) }
1863
+ : isRead
1864
+ ? {
1865
+ ...asRecord(selectNamedFields(normalized, args["fields"])),
1866
+ ...(normalized["page"] ? { page: normalized["page"] } : {}),
1867
+ }
1868
+ : normalized;
1869
+ return yield* signContinuations(selected, ctx);
1870
+ }).pipe(Effect.catchDefect((defect) => defect instanceof ConnectorCallError ? Effect.fail(new Refusal({ error: defect })) : Effect.die(defect))),
1871
+ };
1872
+ return isRead
1873
+ ? read(connection, spec)
1874
+ : write(connection, { ...spec, destructive: declaration.annotations.destructiveHint !== false });
1875
+ });
1876
+ }
1877
+ /** Field selection narrows data while retaining every selected object's cut disclosure. */
1878
+ function selectNamedFields(value, fields) {
1879
+ const restore = (source, selected) => {
1880
+ if (Array.isArray(source) && Array.isArray(selected))
1881
+ return selected.map((item, index) => restore(source[index], item));
1882
+ if (!source || !selected || typeof source !== "object" || typeof selected !== "object")
1883
+ return selected;
1884
+ const original = asRecord(source);
1885
+ const out = Object.fromEntries(Object.entries(selected).map(([key, item]) => [key, restore(original[key], item)]));
1886
+ if (Array.isArray(original["truncated"]))
1887
+ out["truncated"] = original["truncated"];
1888
+ return out;
1889
+ };
1890
+ return restore(value, selectFields(value, fields));
1891
+ }
1892
+ /** Size reports name the cut paths; content cursors retain their independent offsets. */
1893
+ function normalizeResult(value, tool) {
1894
+ const allCuts = new Set();
1895
+ const normalize = (value, path = "") => {
1896
+ if (Array.isArray(value))
1897
+ return value.map((item) => normalize(item, `${path}[]`));
1898
+ if (!value || typeof value !== "object")
1899
+ return value;
1900
+ const out = {};
1901
+ const cut = [];
1902
+ const source = asRecord(value);
1903
+ for (const [key, item] of Object.entries(source)) {
1904
+ if (key === "truncated" && item === true) {
1905
+ if (source["textCursor"] || source["textLength"])
1906
+ cut.push("text");
1907
+ if (source["notesCursor"])
1908
+ cut.push("notes");
1909
+ if (!cut.length)
1910
+ cut.push(...Object.keys(source).filter((name) => typeof source[name] === "string" && !/Id$|Cursor$/.test(name)));
1911
+ }
1912
+ else if (key === "cut" && Array.isArray(item))
1913
+ cut.push(...item.map(String));
1914
+ else if (["elementsNotShown", "layoutsNotShown", "repliesNotShown", "revisionIdNotShown"].includes(key)) {
1915
+ if (item)
1916
+ cut.push(key === "revisionIdNotShown" ? "revisionId" : key.replace("NotShown", "[]"));
1917
+ }
1918
+ else {
1919
+ out[key] = normalize(item, path ? `${path}.${key}` : key);
1920
+ if (typeof item === "string" && /\[… \d+ more characters[; ]/.test(item))
1921
+ cut.push(key);
1922
+ }
1923
+ }
1924
+ if (cut.length) {
1925
+ out["truncated"] = [...new Set(cut)];
1926
+ for (const field of cut)
1927
+ allCuts.add(path ? `${path}.${field}` : field);
1928
+ }
1929
+ return out;
1930
+ };
1931
+ const result = asRecord(normalize(value));
1932
+ if (allCuts.size)
1933
+ result["truncated"] = [...allCuts];
1934
+ if (tool === "list_layouts" || tool === "list_comments") {
1935
+ const noun = tool === "list_layouts" ? "layouts" : "threads";
1936
+ result["items"] = result[noun];
1937
+ delete result[noun];
1938
+ if (allCuts.size)
1939
+ result["truncated"] = [...allCuts].map((path) => path.replace(`${noun}[]`, "items[]"));
1940
+ if (typeof result["total"] === "number") {
1941
+ result["page"] = { ...asRecord(result["page"]), total: result["total"] };
1942
+ delete result["total"];
1943
+ }
1944
+ }
1945
+ return result;
1946
+ }
1947
+ /** Cross-tool content cursors share one signed codec, bound to connection, owner and delegated caller. */
1948
+ function signContinuations(value, ctx) {
1949
+ return Effect.gen(function* () {
1950
+ if (Array.isArray(value)) {
1951
+ const rows = [];
1952
+ for (const item of value)
1953
+ rows.push(yield* signContinuations(item, ctx));
1954
+ return rows;
1955
+ }
1956
+ if (!value || typeof value !== "object")
1957
+ return value;
1958
+ const out = {};
1959
+ for (const [key, item] of Object.entries(value)) {
1960
+ if (key.endsWith("Cursor") && typeof item === "string") {
1961
+ out[key] = yield* boundCursor.encode("slides-content-v1", {}, item, (json) => encodeForAgent(ctx, json, { url: true, claims: true }));
1962
+ }
1963
+ else
1964
+ out[key] = yield* signContinuations(item, ctx);
1965
+ }
1966
+ return out;
1967
+ });
1968
+ }