@zackbart/connecta 0.34.1 → 0.35.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 (429) hide show
  1. package/CHANGELOG.md +363 -0
  2. package/README.md +62 -106
  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 +2051 -1160
  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 +663 -271
  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 +12 -7
  53. package/dist/connectors/api-connector.js +142 -56
  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 +228 -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 +383 -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/agents.d.ts +5 -4
  109. package/dist/operator-ui/agents.js +17 -7
  110. package/dist/operator-ui/config-summary.js +5 -4
  111. package/dist/operator-ui/connection-map.js +7 -5
  112. package/dist/operator-ui/contract.d.ts +7 -3
  113. package/dist/operator-ui/endpoints.js +4 -13
  114. package/dist/operator-ui/fix-prompts.d.ts +1 -1
  115. package/dist/operator-ui/fix-prompts.js +0 -8
  116. package/dist/operator-ui/generated.js +3 -3
  117. package/dist/operator-ui/model.d.ts +2 -3
  118. package/dist/operator-ui/overview.js +5 -2
  119. package/dist/operator-ui/setup-commands.js +0 -4
  120. package/dist/operator-ui/view.d.ts +2 -25
  121. package/dist/operator-ui/view.js +2 -76
  122. package/dist/program-writes.js +47 -26
  123. package/dist/provider-call-facts.d.ts +115 -0
  124. package/dist/provider-call-facts.js +182 -0
  125. package/dist/provider-call.d.ts +112 -0
  126. package/dist/provider-call.js +271 -0
  127. package/dist/provider.d.ts +65 -31
  128. package/dist/provider.js +116 -39
  129. package/dist/providers/_shared/google/hatch.js +91 -0
  130. package/dist/providers/_shared/google/kit.js +537 -0
  131. package/dist/providers/_shared/google/result-size.js +15 -2
  132. package/dist/providers/_shared/google/service-account.d.ts +2 -1
  133. package/dist/providers/_shared/google/service-account.js +8 -4
  134. package/dist/providers/_shared/google/workspace.d.ts +6 -210
  135. package/dist/providers/_shared/google/workspace.js +6 -425
  136. package/dist/providers/_shared/rest/compact-index.js +86 -0
  137. package/dist/providers/_shared/rest/dispatch.js +3 -23
  138. package/dist/providers/_shared/rest/kit.js +546 -0
  139. package/dist/providers/_shared/rest/operation-index.js +230 -14
  140. package/dist/providers/_shared/rest/response-media.js +11 -0
  141. package/dist/providers/_shared/rest/tools.js +101 -78
  142. package/dist/providers/_shared/rest/value-safety.js +353 -195
  143. package/dist/providers/_shared/sensitive-fields.js +27 -0
  144. package/dist/providers/breeze/api.js +584 -0
  145. package/dist/providers/breeze/index.d.ts +1 -3
  146. package/dist/providers/breeze/index.js +67 -1420
  147. package/dist/providers/breeze/openapi.generated.js +60 -0
  148. package/dist/providers/breeze/skill.generated.js +2 -6
  149. package/dist/providers/breeze/tools.js +696 -0
  150. package/dist/providers/ccb/api.js +496 -0
  151. package/dist/providers/ccb/index.d.ts +15 -37
  152. package/dist/providers/ccb/index.js +77 -1702
  153. package/dist/providers/ccb/lib/constants.d.ts +34 -0
  154. package/dist/providers/ccb/lib/constants.js +74 -0
  155. package/dist/providers/ccb/lib/guide.js +9 -0
  156. package/dist/providers/ccb/lib/verdicts.js +387 -0
  157. package/dist/providers/ccb/openapi.generated.js +397 -0
  158. package/dist/providers/ccb/safety.js +295 -0
  159. package/dist/providers/ccb/skill.generated.js +2 -6
  160. package/dist/providers/ccb/tools.js +853 -0
  161. package/dist/providers/cloudflare/api.js +1066 -0
  162. package/dist/providers/cloudflare/index.d.ts +5 -22
  163. package/dist/providers/cloudflare/index.js +79 -197
  164. package/dist/providers/cloudflare/lib/config.d.ts +16 -0
  165. package/dist/providers/cloudflare/lib/config.js +2 -0
  166. package/dist/providers/cloudflare/lib/discovery.js +29 -0
  167. package/dist/providers/cloudflare/{graphql.js → lib/graphql.js} +1 -1
  168. package/dist/providers/cloudflare/lib/hatch.js +103 -0
  169. package/dist/providers/cloudflare/lib/path-bounds.js +28 -0
  170. package/dist/providers/cloudflare/lib/response-media.js +75 -0
  171. package/dist/providers/cloudflare/openapi.generated.js +4 -3
  172. package/dist/providers/cloudflare/{value-safety.js → safety.js} +487 -0
  173. package/dist/providers/cloudflare/skill.generated.js +2 -10
  174. package/dist/providers/cloudflare/tools.js +360 -0
  175. package/dist/providers/docs/api.js +51 -0
  176. package/dist/providers/docs/index.d.ts +1 -12
  177. package/dist/providers/docs/index.js +53 -1165
  178. package/dist/providers/docs/lib/constants.d.ts +18 -0
  179. package/dist/providers/docs/lib/constants.js +18 -0
  180. package/dist/providers/docs/lib/discovery.js +26 -0
  181. package/dist/providers/docs/lib/requests.js +48 -0
  182. package/dist/providers/docs/lib/schemas.js +168 -0
  183. package/dist/providers/docs/openapi.generated.js +16 -0
  184. package/dist/providers/docs/safety.js +54 -0
  185. package/dist/providers/docs/skill.generated.js +2 -6
  186. package/dist/providers/docs/tools.js +802 -0
  187. package/dist/providers/drive/api.js +177 -0
  188. package/dist/providers/drive/index.d.ts +1 -12
  189. package/dist/providers/drive/index.js +38 -1448
  190. package/dist/providers/drive/lib/constants.d.ts +6 -0
  191. package/dist/providers/drive/lib/constants.js +6 -0
  192. package/dist/providers/drive/lib/contracts.js +532 -0
  193. package/dist/providers/drive/lib/hatch.js +197 -0
  194. package/dist/providers/drive/openapi.generated.js +77 -0
  195. package/dist/providers/drive/safety.js +90 -0
  196. package/dist/providers/drive/skill.generated.js +2 -6
  197. package/dist/providers/drive/tools.js +1290 -0
  198. package/dist/providers/forms/api.js +71 -0
  199. package/dist/providers/forms/index.d.ts +1 -12
  200. package/dist/providers/forms/index.js +49 -1145
  201. package/dist/providers/forms/lib/constants.d.ts +14 -0
  202. package/dist/providers/forms/lib/constants.js +19 -0
  203. package/dist/providers/forms/lib/hatch.js +72 -0
  204. package/dist/providers/forms/openapi.generated.js +23 -0
  205. package/dist/providers/forms/safety.js +34 -0
  206. package/dist/providers/forms/skill.generated.js +2 -8
  207. package/dist/providers/forms/tools.js +1024 -0
  208. package/dist/providers/github/api.js +150 -0
  209. package/dist/providers/github/index.d.ts +4 -5
  210. package/dist/providers/github/index.js +30 -87
  211. package/dist/providers/github/{auth.d.ts → lib/auth.d.ts} +5 -3
  212. package/dist/providers/github/{auth.js → lib/auth.js} +40 -43
  213. package/dist/providers/github/lib/discovery.js +132 -0
  214. package/dist/providers/github/lib/operations.js +58 -0
  215. package/dist/providers/github/{scope.d.ts → lib/scope.d.ts} +2 -0
  216. package/dist/providers/github/{scope.js → lib/scope.js} +25 -1
  217. package/dist/providers/github/openapi.generated.js +1216 -0
  218. package/dist/providers/github/safety.js +216 -0
  219. package/dist/providers/github/skill.generated.js +2 -5
  220. package/dist/providers/github/tools.js +506 -0
  221. package/dist/providers/gmail/api.js +114 -0
  222. package/dist/providers/gmail/index.d.ts +1 -12
  223. package/dist/providers/gmail/index.js +73 -1621
  224. package/dist/providers/gmail/lib/constants.d.ts +13 -0
  225. package/dist/providers/gmail/lib/constants.js +16 -0
  226. package/dist/providers/gmail/lib/index.js +173 -0
  227. package/dist/providers/gmail/lib/schemas.js +176 -0
  228. package/dist/providers/gmail/lib/threading.js +120 -0
  229. package/dist/providers/gmail/openapi.generated.js +92 -0
  230. package/dist/providers/gmail/safety.js +21 -0
  231. package/dist/providers/gmail/skill.generated.js +2 -6
  232. package/dist/providers/gmail/tools.js +1435 -0
  233. package/dist/providers/overflow/api.js +1008 -0
  234. package/dist/providers/overflow/index.d.ts +1 -17
  235. package/dist/providers/overflow/index.js +60 -1488
  236. package/dist/providers/overflow/skill.generated.js +2 -5
  237. package/dist/providers/overflow/tools.js +873 -0
  238. package/dist/providers/planning-center/api.js +467 -0
  239. package/dist/providers/planning-center/index.d.ts +4 -35
  240. package/dist/providers/planning-center/index.js +69 -2445
  241. package/dist/providers/planning-center/lib/constants.d.ts +58 -0
  242. package/dist/providers/planning-center/lib/constants.js +83 -0
  243. package/dist/providers/planning-center/lib/discovery.js +40 -0
  244. package/dist/providers/planning-center/lib/guide.js +39 -0
  245. package/dist/providers/planning-center/lib/json-api.js +83 -0
  246. package/dist/providers/planning-center/lib/verdicts.js +1758 -0
  247. package/dist/providers/planning-center/openapi.generated.js +1764 -0
  248. package/dist/providers/planning-center/safety.js +188 -0
  249. package/dist/providers/planning-center/skill.generated.js +2 -27
  250. package/dist/providers/planning-center/tools.js +1010 -0
  251. package/dist/providers/sheets/api.js +133 -0
  252. package/dist/providers/sheets/index.d.ts +4 -16
  253. package/dist/providers/sheets/index.js +61 -1172
  254. package/dist/providers/sheets/lib/constants.d.ts +14 -0
  255. package/dist/providers/sheets/lib/constants.js +14 -0
  256. package/dist/providers/sheets/lib/hatch.js +163 -0
  257. package/dist/providers/sheets/lib/requests.js +105 -0
  258. package/dist/providers/sheets/lib/schemas.js +138 -0
  259. package/dist/providers/sheets/openapi.generated.js +30 -0
  260. package/dist/providers/sheets/safety.js +27 -0
  261. package/dist/providers/sheets/skill.generated.js +2 -7
  262. package/dist/providers/sheets/tools.js +734 -0
  263. package/dist/providers/slides/api.js +147 -0
  264. package/dist/providers/slides/index.d.ts +2 -10
  265. package/dist/providers/slides/index.js +48 -2555
  266. package/dist/providers/slides/lib/constants.d.ts +11 -0
  267. package/dist/providers/slides/lib/constants.js +11 -0
  268. package/dist/providers/slides/lib/requests.js +60 -0
  269. package/dist/providers/slides/lib/schemas.js +565 -0
  270. package/dist/providers/slides/openapi.generated.js +18 -0
  271. package/dist/providers/slides/skill.generated.js +2 -6
  272. package/dist/providers/slides/tools.js +1968 -0
  273. package/dist/providers/stripe/api.js +401 -0
  274. package/dist/providers/stripe/index.d.ts +5 -27
  275. package/dist/providers/stripe/index.js +74 -210
  276. package/dist/providers/stripe/lib/config.d.ts +4 -0
  277. package/dist/providers/stripe/lib/config.js +2 -0
  278. package/dist/providers/stripe/openapi.generated.js +4 -3
  279. package/dist/providers/stripe/{value-safety.js → safety.js} +5 -5
  280. package/dist/providers/stripe/skill.generated.js +2 -11
  281. package/dist/providers/stripe/tools.js +77 -0
  282. package/dist/providers/tithely/api.js +531 -0
  283. package/dist/providers/tithely/index.d.ts +17 -0
  284. package/dist/providers/tithely/index.js +86 -965
  285. package/dist/providers/tithely/skill.generated.js +2 -5
  286. package/dist/providers/tithely/tools.js +426 -0
  287. package/dist/providers/vercel/api.js +551 -0
  288. package/dist/providers/vercel/index.d.ts +9 -25
  289. package/dist/providers/vercel/index.js +66 -260
  290. package/dist/providers/vercel/lib/config.d.ts +2 -0
  291. package/dist/providers/vercel/lib/config.js +2 -0
  292. package/dist/providers/vercel/lib/index.js +117 -0
  293. package/dist/providers/vercel/lib/logs.js +100 -0
  294. package/dist/providers/vercel/lib/pin.js +270 -0
  295. package/dist/providers/vercel/openapi.generated.js +10 -5
  296. package/dist/providers/vercel/{value-safety.js → safety.js} +247 -38
  297. package/dist/providers/vercel/skill.generated.js +2 -7
  298. package/dist/providers/vercel/tools.js +642 -0
  299. package/dist/redaction-identity.js +47 -0
  300. package/dist/redaction-marker.js +125 -0
  301. package/dist/registry.d.ts +21 -15
  302. package/dist/registry.js +409 -376
  303. package/dist/routes/access-tokens.js +1 -1
  304. package/dist/routes/actor-labels.js +1 -1
  305. package/dist/routes/connect.js +1 -1
  306. package/dist/routes/dispatch.js +103 -0
  307. package/dist/routes/mcp.js +170 -79
  308. package/dist/routes/oauth.js +3 -1
  309. package/dist/routes/operator.js +2 -2
  310. package/dist/routes/shared.d.ts +5 -4
  311. package/dist/routes/shared.js +244 -177
  312. package/dist/routes/skills.js +10 -8
  313. package/dist/routes/ui-config.js +27 -3
  314. package/dist/routes/ui-data.js +21 -20
  315. package/dist/routes/ui.js +1 -1
  316. package/dist/runtime/call-admission.js +0 -26
  317. package/dist/runtime/call-entry.js +30 -0
  318. package/dist/runtime/call.js +556 -0
  319. package/dist/runtime/connector-scope.js +7 -0
  320. package/dist/runtime/credential-probe.js +34 -0
  321. package/dist/runtime/executor.js +40 -0
  322. package/dist/runtime/native-storage.js +63 -0
  323. package/dist/runtime/registry.js +27 -0
  324. package/dist/runtime/request-scope.js +72 -0
  325. package/dist/runtime/run.js +15 -7
  326. package/dist/runtime/shared-read.js +34 -5
  327. package/dist/runtime/storage.js +34 -21
  328. package/dist/sent-secrets.d.ts +194 -37
  329. package/dist/sent-secrets.js +1743 -340
  330. package/dist/server.js +5 -56
  331. package/dist/skills.js +322 -222
  332. package/dist/sqlite.js +15 -14
  333. package/dist/storage/d1-copy.js +255 -0
  334. package/dist/storage/d1-driver.js +15 -0
  335. package/dist/storage/keys.js +28 -0
  336. package/dist/storage/sql-driver.js +88 -0
  337. package/dist/storage/sql-native.js +544 -0
  338. package/dist/storage/sql-schema.d.ts +6 -0
  339. package/dist/storage/sql-schema.js +15 -0
  340. package/dist/storage/sql.d.ts +14 -20
  341. package/dist/storage/sql.js +29 -535
  342. package/dist/timeout.js +0 -26
  343. package/dist/tool-safety.js +20 -10
  344. package/dist/types.d.ts +7 -109
  345. package/dist/upward.js +3 -0
  346. package/dist/usage-guide.js +3 -1
  347. package/dist/validate.d.ts +4 -2
  348. package/dist/validate.js +86 -9
  349. package/dist/version.d.ts +1 -1
  350. package/dist/version.js +1 -1
  351. package/dist/worker.js +312 -292
  352. package/dist/workspace-subject.js +5 -1
  353. package/dist/write-outcome.js +454 -34
  354. package/documentation/README.md +27 -7
  355. package/documentation/architecture.md +2547 -226
  356. package/documentation/auth.md +821 -98
  357. package/documentation/code-mode.md +49 -16
  358. package/documentation/deploying.md +164 -20
  359. package/documentation/hosted-mcp-migration.md +460 -0
  360. package/documentation/integrating.md +129 -144
  361. package/documentation/meta-tools.md +409 -32
  362. package/documentation/operating.md +27 -3
  363. package/documentation/operator-tests.md +3 -0
  364. package/documentation/operator-ui.md +68 -45
  365. package/documentation/provider-auth-migration.md +16 -63
  366. package/documentation/provider-migration-0.29.md +10 -5
  367. package/documentation/provider-migration-0.35.md +975 -0
  368. package/documentation/providers.md +965 -0
  369. package/examples/worker/AGENTS.md +23 -9
  370. package/examples/worker/README.md +254 -135
  371. package/examples/worker/alchemy.run.ts +116 -0
  372. package/examples/worker/gitignore +10 -0
  373. package/examples/worker/src/connecta.config.ts +23 -21
  374. package/examples/worker/wrangler.jsonc +14 -5
  375. package/guides/connecting-agents.md +157 -0
  376. package/guides/how-it-works.md +156 -0
  377. package/guides/safety-and-trust.md +153 -0
  378. package/package.json +27 -46
  379. package/templates/node/.env.example +3 -0
  380. package/templates/node/README.md +22 -2
  381. package/templates/node/docker-compose.yml +1 -0
  382. package/templates/node/package.json +3 -2
  383. package/templates/node/src/connecta.config.ts +8 -0
  384. package/dist/catalog-drift.d.ts +0 -138
  385. package/dist/catalog-drift.js +0 -578
  386. package/dist/providers/_shared/request-token.js +0 -83
  387. package/dist/providers/_shared/rest/operation-index.d.ts +0 -133
  388. package/dist/providers/_shared/rest/tools.d.ts +0 -141
  389. package/dist/providers/_shared/rest/value-safety.d.ts +0 -201
  390. package/dist/providers/basecamp/index.d.ts +0 -45
  391. package/dist/providers/basecamp/index.js +0 -997
  392. package/dist/providers/basecamp/skill.generated.js +0 -9
  393. package/dist/providers/cloudflare/rest.d.ts +0 -37
  394. package/dist/providers/cloudflare/rest.js +0 -1097
  395. package/dist/providers/github/hosted.js +0 -218
  396. package/dist/providers/github/rest.js +0 -321
  397. package/dist/providers/infisical/index.d.ts +0 -7
  398. package/dist/providers/infisical/index.js +0 -713
  399. package/dist/providers/infisical/skill.generated.js +0 -8
  400. package/dist/providers/intercom/index.d.ts +0 -24
  401. package/dist/providers/intercom/index.js +0 -121
  402. package/dist/providers/intercom/skill.generated.js +0 -8
  403. package/dist/providers/linear/index.d.ts +0 -55
  404. package/dist/providers/linear/index.js +0 -537
  405. package/dist/providers/linear/skill.generated.js +0 -8
  406. package/dist/providers/mixpanel/index.d.ts +0 -59
  407. package/dist/providers/mixpanel/index.js +0 -472
  408. package/dist/providers/mixpanel/operations.js +0 -20
  409. package/dist/providers/mixpanel/rest.js +0 -603
  410. package/dist/providers/mixpanel/skill.generated.js +0 -10
  411. package/dist/providers/mixpanel/value-safety.js +0 -16
  412. package/dist/providers/notion/index.d.ts +0 -67
  413. package/dist/providers/notion/index.js +0 -368
  414. package/dist/providers/notion/openapi.generated.js +0 -75
  415. package/dist/providers/notion/rest.d.ts +0 -24
  416. package/dist/providers/notion/rest.js +0 -1226
  417. package/dist/providers/notion/skill.generated.js +0 -11
  418. package/dist/providers/notion/value-safety.js +0 -56
  419. package/dist/providers/revenuecat/index.d.ts +0 -51
  420. package/dist/providers/revenuecat/index.js +0 -697
  421. package/dist/providers/revenuecat/skill.generated.js +0 -11
  422. package/dist/providers/stripe/rest.d.ts +0 -14
  423. package/dist/providers/stripe/rest.js +0 -402
  424. package/dist/providers/vercel/rest.d.ts +0 -20
  425. package/dist/providers/vercel/rest.js +0 -1020
  426. package/dist/providers/wispr-flow/index.d.ts +0 -14
  427. package/dist/providers/wispr-flow/index.js +0 -117
  428. package/dist/providers/wispr-flow/skill.generated.js +0 -8
  429. /package/dist/providers/github/{key.js → lib/key.js} +0 -0
@@ -0,0 +1,1024 @@
1
+ // Forms projections and revision-checked workflows on the standard provider kit.
2
+ import { Effect, Option, Schema } from "effect";
3
+ import { ConnectorCallError } from "../../errors.js";
4
+ import { clampText, jsonBytes, RESULT_BUDGET_BYTES } from "../_shared/google/result-size.js";
5
+ import { googleClient, googleFailureMessage, googleOutcomeOf } from "../_shared/google/kit.js";
6
+ import { encodeForAgent, redactBeforeCut, redactSentSecrets } from "../../sent-secrets.js";
7
+ import { FORMS } from "./api.js";
8
+ import { Integer, isMaxChars, isMinChars } from "../../http/json-schema.js";
9
+ import { boundCursor, StandardPage } from "../../http/paging.js";
10
+ import { read, write } from "../../http/tools.js";
11
+ import { fieldsInput, fieldsProblem, selectFields } from "../../http/fields.js";
12
+ import { Refusal } from "../../http/errors.js";
13
+ import { Operation } from "../../runtime/call.js";
14
+ import { formsIndex, FORMS_REQUEST_KINDS } from "./lib/hatch.js";
15
+ import { googleBatchGuard } from "../_shared/google/hatch.js";
16
+ /** Google's own response page maximum is 5,000; a page an agent reads is not. */
17
+ const MAX_PAGE_SIZE = 100;
18
+ const DEFAULT_PAGE_SIZE = 25;
19
+ /** A listed answer is cut here; get_response returns it whole. */
20
+ export const LIST_ANSWER_CHARS = 2_000;
21
+ /** A form or item title or description is cut here in get_form's projection. */
22
+ const DESCRIPTION_CHARS = 2_000;
23
+ /** ...and bounded here in bytes, so escaped control characters cannot grow it. */
24
+ const DESCRIPTION_BYTES = 8 * 1024;
25
+ /** Options shown per choice question; a long dropdown says how many it hid. */
26
+ const MAX_OPTIONS = 100;
27
+ /** An option or grid row is cut here; forms_api_read reads it whole. */
28
+ const MAX_LABEL_CHARS = 300;
29
+ const MAX_LABEL_BYTES = 1024;
30
+ /** One answer value in get_response, before it would crowd out the rest. */
31
+ const MAX_ANSWER_CHARS = 50_000;
32
+ /** Question ids one get_response call may name. */
33
+ const MAX_QUESTION_IDS = 100;
34
+ /** Requests in one batch_update_form call. */
35
+ const MAX_REQUESTS = 100;
36
+ /**
37
+ * Room a filling page keeps for its own fields and its cursor. Every result
38
+ * stays inside the shared RESULT_BUDGET_BYTES (`./google/result-size.ts`), so
39
+ * it crosses execute_code's host bridge and a direct call alike; a page ends
40
+ * early, with a cursor, rather than grow past it, and the complete result,
41
+ * cursor included, is measured before it is returned.
42
+ */
43
+ const PAGE_RESERVE_BYTES = 4 * 1024;
44
+ function asRecord(value) {
45
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
46
+ }
47
+ function asArray(value) {
48
+ return Array.isArray(value) ? value : [];
49
+ }
50
+ function compact(value) {
51
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
52
+ }
53
+ function text(value) {
54
+ return typeof value === "string" && value !== "" ? value : undefined;
55
+ }
56
+ function integer(value) {
57
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
58
+ }
59
+ /** Cut text at `max` characters and say so in the text itself. Every secret the request sent
60
+ * is replaced first: a cut through an echo leaves a fragment no later redaction finds. */
61
+ function capped(ctx, whole, max, remedy) {
62
+ if (whole === undefined || whole.length <= max)
63
+ return whole;
64
+ const value = redactBeforeCut(ctx, whole);
65
+ if (value.length <= max)
66
+ return value;
67
+ // Never split a surrogate pair: a lone high surrogate before the marker
68
+ // would make the text ill-formed.
69
+ let cut = value.slice(0, max);
70
+ const last = cut.charCodeAt(cut.length - 1);
71
+ if (last >= 0xd800 && last <= 0xdbff)
72
+ cut = cut.slice(0, -1);
73
+ return `${cut}\n[… ${value.length - cut.length} more characters truncated; ${remedy}]`;
74
+ }
75
+ /**
76
+ * Text cut at `chars` characters, then held to `bytes` of JSON — quotes and
77
+ * escapes counted — so a run of control characters cannot outgrow its place.
78
+ */
79
+ function bounded(ctx, value, chars, bytes) {
80
+ const cut = capped(ctx, value, chars, "use forms_api_read to read it");
81
+ if (cut === undefined || jsonBytes(cut) <= bytes)
82
+ return cut;
83
+ return clampText(ctx, value, bytes, (dropped) => `\n[… ${dropped} more characters truncated; use forms_api_read to read it]`);
84
+ }
85
+ function editUrl(formId) {
86
+ return `https://docs.google.com/forms/d/${encodeURIComponent(formId)}/edit`;
87
+ }
88
+ const encoder = new TextEncoder();
89
+ function base64Url(bytes) {
90
+ let binary = "";
91
+ for (const byte of bytes)
92
+ binary += String.fromCharCode(byte);
93
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
94
+ }
95
+ /** What a cursor is bound to: its tool and every argument that decides its pages. */
96
+ async function digestOf(bound) {
97
+ const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(JSON.stringify(bound))));
98
+ return base64Url(hash.subarray(0, 16));
99
+ }
100
+ /**
101
+ * A cursor of connecta's claims. An agent decodes it and no later pass reads
102
+ * through its encoding, so Google's text enters only redacted: a page token
103
+ * it must send back, redacted ({@link redactSentSecrets}); a value it only
104
+ * compares, by {@link claimOf}.
105
+ */
106
+ function encodeCursor(ctx, tool, args, cursor) {
107
+ return boundCursor.encode(tool, args, JSON.stringify(cursor), (json) => encodeForAgent(ctx, json, { url: true, claims: true }));
108
+ }
109
+ /** A downstream value a cursor only compares: itself, or its digest when it holds a sent value. */
110
+ async function claimOf(ctx, value) {
111
+ return redactSentSecrets(ctx, value) === value ? value : `#${await digestOf(["claim", value])}`;
112
+ }
113
+ /**
114
+ * A cursor this tool issued for these same arguments, or a refusal: one
115
+ * carried to another form, filter, or tool would page through something else.
116
+ */
117
+ function decodeCursor(value, digest, keys, valid, again) {
118
+ let cursor;
119
+ try {
120
+ cursor = asRecord(JSON.parse(value));
121
+ }
122
+ catch {
123
+ cursor = {};
124
+ }
125
+ const shaped = Object.keys(cursor).length === keys.length && keys.every((key) => key in cursor);
126
+ if (!shaped || cursor["d"] !== digest || !valid(cursor)) {
127
+ throw new ConnectorCallError("invalid_args", `cursor is not one this tool returned for these arguments. Pass page.nextCursor back unchanged, ${again}.`);
128
+ }
129
+ return cursor;
130
+ }
131
+ function isIndex(value) {
132
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
133
+ }
134
+ const CHOICE_TYPES = {
135
+ RADIO: "radio",
136
+ CHECKBOX: "checkbox",
137
+ DROP_DOWN: "drop_down",
138
+ };
139
+ function choiceOptions(ctx, choice) {
140
+ const all = asArray(choice["options"]).map(asRecord);
141
+ const hasOther = all.some((option) => option["isOther"] === true);
142
+ const values = all
143
+ .filter((option) => option["isOther"] !== true)
144
+ .map((option) => bounded(ctx, String(option["value"] ?? ""), MAX_LABEL_CHARS, MAX_LABEL_BYTES) ?? "");
145
+ return compact({
146
+ options: values.slice(0, MAX_OPTIONS),
147
+ moreOptions: values.length > MAX_OPTIONS ? values.length - MAX_OPTIONS : undefined,
148
+ hasOther: hasOther || undefined,
149
+ });
150
+ }
151
+ /** One question's id, type, and the shape of what it accepts. */
152
+ function projectQuestion(ctx, value, rowTitle) {
153
+ const question = asRecord(value);
154
+ const grading = asRecord(question["grading"]);
155
+ const base = {
156
+ questionId: text(question["questionId"]),
157
+ rowTitle,
158
+ required: question["required"] === true,
159
+ points: integer(grading["pointValue"]),
160
+ };
161
+ if (question["choiceQuestion"]) {
162
+ const choice = asRecord(question["choiceQuestion"]);
163
+ return compact({ ...base, type: CHOICE_TYPES[String(choice["type"])] ?? "unknown", ...choiceOptions(ctx, choice) });
164
+ }
165
+ if (question["textQuestion"]) {
166
+ return compact({
167
+ ...base,
168
+ type: asRecord(question["textQuestion"])["paragraph"] === true ? "paragraph" : "short_text",
169
+ });
170
+ }
171
+ if (question["scaleQuestion"]) {
172
+ const scale = asRecord(question["scaleQuestion"]);
173
+ return compact({
174
+ ...base,
175
+ type: "scale",
176
+ scale: compact({
177
+ low: integer(scale["low"]),
178
+ high: integer(scale["high"]),
179
+ lowLabel: text(scale["lowLabel"]),
180
+ highLabel: text(scale["highLabel"]),
181
+ }),
182
+ });
183
+ }
184
+ if (question["dateQuestion"]) {
185
+ const date = asRecord(question["dateQuestion"]);
186
+ return compact({
187
+ ...base,
188
+ type: "date",
189
+ includesYear: date["includeYear"] === true,
190
+ includesTime: date["includeTime"] === true,
191
+ });
192
+ }
193
+ if (question["timeQuestion"]) {
194
+ return compact({ ...base, type: "time", duration: asRecord(question["timeQuestion"])["duration"] === true });
195
+ }
196
+ if (question["ratingQuestion"]) {
197
+ const rating = asRecord(question["ratingQuestion"]);
198
+ return compact({ ...base, type: "rating", scale: compact({ low: 1, high: integer(rating["ratingScaleLevel"]) }) });
199
+ }
200
+ if (question["fileUploadQuestion"])
201
+ return compact({ ...base, type: "file_upload" });
202
+ if (question["rowQuestion"])
203
+ return compact({ ...base, type: "grid_row" });
204
+ return compact({ ...base, type: "unknown" });
205
+ }
206
+ /**
207
+ * One item, with every question it asks under `questions`: one for a question
208
+ * item, one per row for a grid, none for a section break or media. Answers are
209
+ * keyed by question id, so this is the map a response is read through.
210
+ */
211
+ function projectItem(ctx, value) {
212
+ const item = asRecord(value);
213
+ const base = {
214
+ itemId: text(item["itemId"]),
215
+ title: bounded(ctx, text(item["title"]), DESCRIPTION_CHARS, DESCRIPTION_BYTES),
216
+ description: bounded(ctx, text(item["description"]), DESCRIPTION_CHARS, DESCRIPTION_BYTES),
217
+ };
218
+ if (item["questionItem"]) {
219
+ return compact({
220
+ ...base,
221
+ kind: "question",
222
+ questions: [projectQuestion(ctx, asRecord(item["questionItem"])["question"])],
223
+ });
224
+ }
225
+ if (item["questionGroupItem"]) {
226
+ const group = asRecord(item["questionGroupItem"]);
227
+ const grid = asRecord(group["grid"]);
228
+ const columns = asRecord(grid["columns"]);
229
+ return compact({
230
+ ...base,
231
+ kind: "question_group",
232
+ // A grid's rows share its columns: the choices are stated once, here.
233
+ gridType: group["grid"] ? (CHOICE_TYPES[String(columns["type"])] ?? "unknown") : undefined,
234
+ ...(group["grid"] ? choiceOptions(ctx, columns) : {}),
235
+ questions: asArray(group["questions"]).map((question) => projectQuestion(ctx, question, bounded(ctx, text(asRecord(asRecord(question)["rowQuestion"])["title"]), MAX_LABEL_CHARS, MAX_LABEL_BYTES))),
236
+ });
237
+ }
238
+ const kind = item["pageBreakItem"]
239
+ ? "page_break"
240
+ : item["textItem"]
241
+ ? "text"
242
+ : item["imageItem"]
243
+ ? "image"
244
+ : item["videoItem"]
245
+ ? "video"
246
+ : "unknown";
247
+ return compact({ ...base, kind });
248
+ }
249
+ function projectForm(ctx, value) {
250
+ const form = asRecord(value);
251
+ const info = asRecord(form["info"]);
252
+ const settings = asRecord(form["settings"]);
253
+ const publish = asRecord(asRecord(form["publishSettings"])["publishState"]);
254
+ const formId = text(form["formId"]);
255
+ const items = asArray(form["items"]).map((item) => projectItem(ctx, item));
256
+ return compact({
257
+ formId,
258
+ title: bounded(ctx, text(info["title"]), DESCRIPTION_CHARS, DESCRIPTION_BYTES),
259
+ documentTitle: bounded(ctx, text(info["documentTitle"]), DESCRIPTION_CHARS, DESCRIPTION_BYTES),
260
+ description: bounded(ctx, text(info["description"]), DESCRIPTION_CHARS, DESCRIPTION_BYTES),
261
+ revisionId: text(form["revisionId"]),
262
+ responderUri: text(form["responderUri"]),
263
+ editUrl: formId ? editUrl(formId) : undefined,
264
+ linkedSheetId: text(form["linkedSheetId"]),
265
+ isQuiz: asRecord(settings["quizSettings"])["isQuiz"] === true,
266
+ emailCollection: text(settings["emailCollectionType"]),
267
+ // Absent on a legacy form, which has no publish settings at all.
268
+ published: form["publishSettings"] ? publish["isPublished"] === true : undefined,
269
+ acceptingResponses: form["publishSettings"] ? publish["isAcceptingResponses"] === true : undefined,
270
+ itemCount: items.length,
271
+ questionCount: items.reduce((count, item) => count + asArray(item["questions"]).length, 0),
272
+ items,
273
+ });
274
+ }
275
+ /**
276
+ * An item cut to `budget` bytes. Its own text and every option and label are
277
+ * already bounded (an item without questions stays under ~120 KiB), so only a
278
+ * grid's rows can outgrow a page; the rows that fit are kept and the rest
279
+ * counted in `moreQuestions`, and the page's cursor continues inside this
280
+ * item from the first row it left out. The caller still measures what it
281
+ * returns.
282
+ */
283
+ function fitItem(item, budget) {
284
+ if (jsonBytes(item) <= budget)
285
+ return item;
286
+ const questions = asArray(item["questions"]);
287
+ const kept = [];
288
+ let used = jsonBytes({ ...item, questions: [], moreQuestions: questions.length });
289
+ for (const question of questions) {
290
+ const size = jsonBytes(question) + 1;
291
+ if (used + size > budget)
292
+ break;
293
+ kept.push(question);
294
+ used += size;
295
+ }
296
+ return { ...item, questions: kept, moreQuestions: questions.length - kept.length };
297
+ }
298
+ /**
299
+ * An item's questions from `from` on, for a page that continues inside it.
300
+ * Its description and options went out with its first part; the continuation
301
+ * carries what places the rows — its id, kind, and title — and where they
302
+ * start, in `questionsFrom`.
303
+ */
304
+ function continuedItem(item, from) {
305
+ if (from === 0)
306
+ return item;
307
+ return compact({
308
+ itemId: item["itemId"],
309
+ kind: item["kind"],
310
+ title: item["title"],
311
+ questionsFrom: from,
312
+ questions: asArray(item["questions"]).slice(from),
313
+ });
314
+ }
315
+ /**
316
+ * Every question id in the form, labeled the way a person reads it — the
317
+ * item's title, and the row's for a grid — in the form's own order.
318
+ */
319
+ function questionLabels(ctx, form) {
320
+ const labels = new Map();
321
+ for (const value of asArray(asRecord(form)["items"])) {
322
+ const item = projectItem(ctx, value);
323
+ for (const question of asArray(item["questions"]).map(asRecord)) {
324
+ const id = text(question["questionId"]);
325
+ if (!id)
326
+ continue;
327
+ const title = question["rowTitle"] ? `${item["title"] ?? ""} [${question["rowTitle"]}]`.trim() : item["title"];
328
+ labels.set(id, { title, order: labels.size });
329
+ }
330
+ }
331
+ return labels;
332
+ }
333
+ /**
334
+ * A grader's feedback on one answer: its text, and the links or videos it
335
+ * points to, each as the address a person opens and the label they were shown.
336
+ */
337
+ function projectFeedback(ctx, value, maxChars, remedy) {
338
+ if (!value || typeof value !== "object")
339
+ return undefined;
340
+ const feedback = asRecord(value);
341
+ const links = asArray(feedback["material"]).map((entry) => {
342
+ const material = asRecord(entry);
343
+ const link = asRecord(material["link"]);
344
+ const video = asRecord(material["video"]);
345
+ return compact({
346
+ uri: text(link["uri"]) ?? text(video["youtubeUri"]),
347
+ displayText: text(link["displayText"]) ?? text(video["displayText"]),
348
+ video: material["video"] ? true : undefined,
349
+ });
350
+ });
351
+ const said = text(feedback["text"]);
352
+ return compact({
353
+ text: maxChars === undefined ? said : capped(ctx, said, maxChars, remedy),
354
+ links: links.length > 0 ? links : undefined,
355
+ });
356
+ }
357
+ function projectResponse(ctx, value, labels, maxChars, remedy) {
358
+ const response = asRecord(value);
359
+ const answers = Object.entries(asRecord(response["answers"])).map(([key, raw]) => {
360
+ const answer = asRecord(raw);
361
+ const questionId = text(answer["questionId"]) ?? key;
362
+ const grade = asRecord(answer["grade"]);
363
+ const values = asArray(asRecord(answer["textAnswers"])["answers"]).map((entry) => {
364
+ const value = String(asRecord(entry)["value"] ?? "");
365
+ return maxChars === undefined ? value : capped(ctx, value, maxChars, remedy);
366
+ });
367
+ const files = asArray(asRecord(answer["fileUploadAnswers"])["answers"]).map((entry) => {
368
+ const file = asRecord(entry);
369
+ return compact({
370
+ fileId: text(file["fileId"]),
371
+ fileName: text(file["fileName"]),
372
+ mimeType: text(file["mimeType"]),
373
+ });
374
+ });
375
+ return compact({
376
+ questionId,
377
+ // Undefined for a question deleted since this response was submitted.
378
+ title: labels.get(questionId)?.title,
379
+ values: files.length > 0 && values.length === 0 ? undefined : values,
380
+ files: files.length > 0 ? files : undefined,
381
+ score: typeof grade["score"] === "number" ? grade["score"] : undefined,
382
+ correct: typeof grade["correct"] === "boolean" ? grade["correct"] : undefined,
383
+ feedback: projectFeedback(ctx, grade["feedback"], maxChars, remedy),
384
+ });
385
+ });
386
+ // The form's order, then anything the form no longer asks.
387
+ const rank = (id) => labels.get(id)?.order ?? Number.MAX_SAFE_INTEGER;
388
+ answers.sort((a, b) => rank(a.questionId) - rank(b.questionId));
389
+ return compact({
390
+ responseId: text(response["responseId"]),
391
+ createTime: text(response["createTime"]),
392
+ lastSubmittedTime: text(response["lastSubmittedTime"]),
393
+ respondentEmail: text(response["respondentEmail"]),
394
+ totalScore: typeof response["totalScore"] === "number" ? response["totalScore"] : undefined,
395
+ answers,
396
+ });
397
+ }
398
+ /**
399
+ * A response projected inside `budget` bytes: answers cut at each of `caps`
400
+ * in turn until it fits, and if even the last does not, answers left out from
401
+ * the end and named in `omittedQuestionIds`, for get_response to read by id.
402
+ */
403
+ function fitResponse(ctx, value, labels, caps, remedy, budget) {
404
+ let projected = {};
405
+ for (const cap of caps) {
406
+ projected = projectResponse(ctx, value, labels, cap, remedy);
407
+ if (jsonBytes(projected) <= budget)
408
+ return projected;
409
+ }
410
+ const answers = asArray(projected["answers"]).map(asRecord);
411
+ const kept = [];
412
+ let used = jsonBytes({
413
+ ...projected,
414
+ answers: [],
415
+ omittedQuestionIds: answers.map((answer) => answer["questionId"]),
416
+ });
417
+ for (const answer of answers) {
418
+ const size = jsonBytes(answer) + 1;
419
+ if (used + size > budget)
420
+ break;
421
+ kept.push(answer);
422
+ used += size;
423
+ }
424
+ return {
425
+ ...projected,
426
+ answers: kept,
427
+ omittedQuestionIds: answers.slice(kept.length).map((answer) => answer["questionId"]),
428
+ };
429
+ }
430
+ // --- Writing ----------------------------------------------------------------------
431
+ /** What a batch reply says it made: the new item and its question ids. */
432
+ function projectReplies(value) {
433
+ return asArray(value).map((entry) => {
434
+ const created = asRecord(asRecord(entry)["createItem"]);
435
+ return asRecord(entry)["createItem"]
436
+ ? compact({
437
+ createdItemId: text(created["itemId"]),
438
+ createdQuestionIds: asArray(created["questionId"]).filter((id) => typeof id === "string"),
439
+ })
440
+ : {};
441
+ });
442
+ }
443
+ /**
444
+ * A batch's reply, inside one result. The write has already applied by the
445
+ * time this runs, so nothing here may read as failure or as nothing changed:
446
+ * a reply too large to return whole — a grid created with thousands of rows
447
+ * names every row's question id — drops the question ids first and keeps each
448
+ * created item's id, and if even that cannot fit, keeps only the counts.
449
+ * Either way `truncated` and `note` say the batch applied and where to read
450
+ * what was left out.
451
+ */
452
+ function projectBatch(formId, value) {
453
+ const result = asRecord(value);
454
+ const revisionId = text(asRecord(result["writeControl"])["requiredRevisionId"]);
455
+ const replies = projectReplies(result["replies"]);
456
+ const whole = compact({ formId, revisionId, replies });
457
+ if (jsonBytes(whole) <= RESULT_BUDGET_BYTES)
458
+ return whole;
459
+ const note = "The batch applied. Its reply was too large to return whole; read the created items' question ids with get_form, following page.nextCursor to the last page.";
460
+ const idsOnly = compact({
461
+ formId,
462
+ revisionId,
463
+ replies: replies.map((reply) => compact({ createdItemId: reply["createdItemId"] })),
464
+ truncated: ["replies[].createdQuestionIds"],
465
+ note,
466
+ });
467
+ if (jsonBytes(idsOnly) <= RESULT_BUDGET_BYTES)
468
+ return idsOnly;
469
+ return compact({
470
+ formId,
471
+ revisionId: revisionId && jsonBytes(revisionId) <= 1024 ? revisionId : undefined,
472
+ replies: [],
473
+ replyCount: replies.length,
474
+ createdItemCount: replies.filter((reply) => reply["createdItemId"] !== undefined).length,
475
+ truncated: ["replies[]", ...(revisionId && jsonBytes(revisionId) > 1024 ? ["revisionId"] : [])],
476
+ note: revisionId && jsonBytes(revisionId) <= 1024 ? note : `${note} Read its revisionId there too.`,
477
+ });
478
+ }
479
+ // --- Effect schemas: catalog and runtime decoding share these declarations. ---
480
+ const pattern = (source) => Schema.makeFilter((value) => new RegExp(source).test(value), { toJsonSchema: () => ({ pattern: source }) });
481
+ const string = (description, max = 256) => Schema.String.check(isMinChars(1), isMaxChars(max)).annotate({ description });
482
+ const id = (description) => string(description).check(pattern("^[A-Za-z0-9_-]+$"));
483
+ const formId = id("Form id from its /forms/d/<formId>/edit URL or Drive; not the /forms/d/e/ responder link.");
484
+ const revision = string("Current revisionId from a read or preceding edit; re-read and rebuild after conflict.").check(pattern("^\\S+$"));
485
+ const cursor = Schema.optionalKey(string("Opaque page.nextCursor from this same tool and arguments.", 16384));
486
+ const optional = (fields) => Schema.Struct(Object.fromEntries(Object.entries(fields).map(([key, value]) => [key, Schema.optionalKey(value)])));
487
+ const Truncated = Schema.Array(Schema.String);
488
+ const File = optional({ fileId: Schema.String, fileName: Schema.String, mimeType: Schema.String });
489
+ const Feedback = optional({
490
+ text: Schema.String,
491
+ links: Schema.Array(optional({ uri: Schema.String, displayText: Schema.String, video: Schema.Boolean })),
492
+ });
493
+ const Answer = optional({
494
+ questionId: Schema.String,
495
+ title: Schema.String,
496
+ values: Schema.Array(Schema.String),
497
+ files: Schema.Array(File),
498
+ score: Schema.Number,
499
+ correct: Schema.Boolean,
500
+ feedback: Feedback,
501
+ });
502
+ const ResponseRecord = optional({
503
+ responseId: Schema.String,
504
+ createTime: Schema.String,
505
+ lastSubmittedTime: Schema.String,
506
+ respondentEmail: Schema.String,
507
+ totalScore: Schema.Number,
508
+ omittedQuestionIds: Schema.Array(Schema.String),
509
+ answers: Schema.Array(Answer),
510
+ truncated: Truncated,
511
+ });
512
+ const choice = { options: Schema.Array(Schema.String), hasOther: Schema.Boolean };
513
+ const Question = optional({
514
+ questionId: Schema.String,
515
+ type: Schema.String,
516
+ rowTitle: Schema.String,
517
+ required: Schema.Boolean,
518
+ points: Integer,
519
+ ...choice,
520
+ scale: optional({ low: Integer, high: Integer, lowLabel: Schema.String, highLabel: Schema.String }),
521
+ includesYear: Schema.Boolean,
522
+ includesTime: Schema.Boolean,
523
+ duration: Schema.Boolean,
524
+ });
525
+ const Item = optional({
526
+ itemId: Schema.String,
527
+ kind: Schema.String,
528
+ title: Schema.String,
529
+ description: Schema.String,
530
+ gridType: Schema.String,
531
+ ...choice,
532
+ questions: Schema.Array(Question),
533
+ questionsFrom: Integer,
534
+ });
535
+ const FormRecord = optional({
536
+ formId: Schema.String,
537
+ title: Schema.String,
538
+ documentTitle: Schema.String,
539
+ description: Schema.String,
540
+ revisionId: Schema.String,
541
+ responderUri: Schema.String,
542
+ editUrl: Schema.String,
543
+ linkedSheetId: Schema.String,
544
+ isQuiz: Schema.Boolean,
545
+ emailCollection: Schema.String,
546
+ published: Schema.Boolean,
547
+ acceptingResponses: Schema.Boolean,
548
+ itemCount: Integer,
549
+ questionCount: Integer,
550
+ page: StandardPage,
551
+ items: Schema.Array(Item),
552
+ truncated: Truncated,
553
+ });
554
+ const BatchRecord = optional({
555
+ formId: Schema.String,
556
+ revisionId: Schema.String,
557
+ replies: Schema.Array(optional({ createdItemId: Schema.String, createdQuestionIds: Schema.Array(Schema.String) })),
558
+ truncated: Truncated,
559
+ note: Schema.String,
560
+ replyCount: Integer,
561
+ createdItemCount: Integer,
562
+ });
563
+ const contextOf = Effect.gen(function* () {
564
+ const operation = yield* Effect.serviceOption(Operation);
565
+ if (Option.isNone(operation) || !operation.value)
566
+ return yield* Effect.die(new Error("Missing Forms call context."));
567
+ return operation.value;
568
+ });
569
+ const knownFailures = Effect.catchDefect((error) => error instanceof ConnectorCallError ? Effect.fail(new Refusal({ error })) : Effect.die(error));
570
+ /** Convert internal continuation counts and text markers into S3 paths. */
571
+ function standardResult(value) {
572
+ const truncated = new Set(Array.isArray(value["truncated"]) ? value["truncated"] : []);
573
+ const visit = (entry, path) => {
574
+ if (typeof entry === "string") {
575
+ if (/\[… .*truncated;/.test(entry))
576
+ truncated.add(path);
577
+ return entry;
578
+ }
579
+ if (Array.isArray(entry))
580
+ return entry.map((item) => visit(item, `${path}[]`));
581
+ if (!entry || typeof entry !== "object")
582
+ return entry;
583
+ const out = {};
584
+ for (const [key, child] of Object.entries(entry)) {
585
+ if (key === "truncated")
586
+ continue;
587
+ if (key === "moreOptions" || key === "moreQuestions") {
588
+ if (child)
589
+ truncated.add(`${path ? `${path}.` : ""}${key === "moreOptions" ? "options" : "questions"}[]`);
590
+ continue;
591
+ }
592
+ if (key === "omittedQuestionIds" && Array.isArray(child) && child.length)
593
+ truncated.add(`${path ? `${path}.` : ""}answers[]`);
594
+ out[key] = visit(child, path ? `${path}.${key}` : key);
595
+ }
596
+ return out;
597
+ };
598
+ const result = visit(value, "");
599
+ return { ...result, ...(truncated.size ? { truncated: [...truncated] } : {}) };
600
+ }
601
+ export function formsTools({ connection }) {
602
+ const client = googleClient(connection, FORMS.provider, { standard: true, index: formsIndex });
603
+ const formPath = (formId) => `/forms/${encodeURIComponent(formId)}`;
604
+ const readForm = (formId, step) => client.json({ method: "GET", path: formPath(formId), template: "/forms/{formId}" }, step ? { step } : undefined);
605
+ const pairedReads = { form: "once", answers: "once" };
606
+ const batch = (formId, requests, revisionId) => Effect.gen(function* () {
607
+ const body = compact({ requests, writeControl: revisionId ? { requiredRevisionId: revisionId } : undefined });
608
+ const problem = googleBatchGuard(body, { kinds: FORMS_REQUEST_KINDS, revision: revisionId !== undefined });
609
+ if (problem)
610
+ return yield* Effect.fail(new Refusal({ error: new ConnectorCallError("invalid_args", problem) }));
611
+ const reply = yield* client.json({ method: "POST", path: `${formPath(formId)}:batchUpdate`, template: "/forms/{formId}:batchUpdate", body }, { revisionGuarded: revisionId !== undefined });
612
+ return projectBatch(formId, reply);
613
+ });
614
+ return [
615
+ read(connection, {
616
+ name: "get_form",
617
+ description: "Get one Google Form: title, description, quiz and publish state, revisionId, and its items with question ids, types, and options, paged when large. Never lists forms.",
618
+ input: Schema.Struct({
619
+ formId,
620
+ cursor,
621
+ fields: Schema.optionalKey(fieldsInput("Dot paths to keep in the form; page and cut metadata is preserved.")),
622
+ }),
623
+ output: FormRecord,
624
+ run: (args) => Effect.gen(function* () {
625
+ const ctx = yield* contextOf;
626
+ const problem = fieldsProblem(args.fields, new Set(Object.keys(FormRecord.fields)), "the form");
627
+ if (problem)
628
+ return yield* Effect.fail(new Refusal({ error: problem }));
629
+ const formId = args["formId"];
630
+ const form = yield* readForm(formId);
631
+ const { items, ...header } = projectForm(ctx, form);
632
+ const all = asArray(items).map(asRecord);
633
+ const revision = yield* Effect.promise(() => claimOf(ctx, text(asRecord(form)["revisionId"]) ?? ""));
634
+ const digest = yield* Effect.promise(() => digestOf(["get_form", formId]));
635
+ // A cursor names an item and, inside a grid too large for one page,
636
+ // the first of its questions not yet returned, so every question id
637
+ // is reachable by paging, however many rows one grid has.
638
+ let start = 0;
639
+ let from = 0;
640
+ if (typeof args["cursor"] === "string") {
641
+ const cursor = decodeCursor(yield* boundCursor.decode("get_form", { formId }, args["cursor"]), digest, ["d", "i", "q", "r"], (c) => isIndex(c["i"]) && isIndex(c["q"]) && typeof c["r"] === "string", "with the same formId");
642
+ // Items are addressed by index and rows by position, so a page of a
643
+ // changed form would skip or repeat them without saying so.
644
+ const at = all[cursor["i"]];
645
+ if (cursor["r"] !== revision ||
646
+ cursor["i"] > all.length ||
647
+ (cursor["q"] > 0 && (at === undefined || cursor["q"] >= asArray(at["questions"]).length))) {
648
+ throw new ConnectorCallError("conflict", "The form changed after the page this cursor continues. Read get_form again from the start, without cursor.");
649
+ }
650
+ start = cursor["i"];
651
+ from = cursor["q"];
652
+ }
653
+ const piece = (index) => (index === start ? continuedItem(all[index], from) : all[index]);
654
+ const kept = [];
655
+ let used = jsonBytes({ ...header, items: [] }) + PAGE_RESERVE_BYTES;
656
+ for (let index = start; index < all.length; index += 1) {
657
+ let item = piece(index);
658
+ let size = jsonBytes(item) + 1;
659
+ if (used + size > RESULT_BUDGET_BYTES) {
660
+ if (kept.length > 0)
661
+ break;
662
+ item = fitItem(item, RESULT_BUDGET_BYTES - used);
663
+ size = jsonBytes(item) + 1;
664
+ }
665
+ kept.push(item);
666
+ used += size;
667
+ // A grid cut short ends the page: the cursor continues inside it.
668
+ if (item["moreQuestions"])
669
+ break;
670
+ }
671
+ const build = () => Effect.gen(function* () {
672
+ const lastIndex = start + kept.length - 1;
673
+ const last = kept[kept.length - 1];
674
+ let next;
675
+ if (last?.["moreQuestions"]) {
676
+ const before = lastIndex === start ? from : 0;
677
+ next = { d: digest, i: lastIndex, q: before + asArray(last["questions"]).length, r: revision };
678
+ }
679
+ else if (lastIndex + 1 < all.length) {
680
+ next = { d: digest, i: lastIndex + 1, q: 0, r: revision };
681
+ }
682
+ const nextCursor = next ? yield* encodeCursor(ctx, "get_form", { formId }, next) : undefined;
683
+ return {
684
+ ...header,
685
+ page: { hasMore: nextCursor !== undefined, ...(nextCursor ? { nextCursor } : {}) },
686
+ items: kept,
687
+ };
688
+ });
689
+ // The estimate above reserved room; the complete result, cursor and
690
+ // all, is what has to fit.
691
+ let result = yield* build();
692
+ while (jsonBytes(result) > RESULT_BUDGET_BYTES && kept.length > 1) {
693
+ kept.pop();
694
+ result = yield* build();
695
+ }
696
+ if (jsonBytes(result) > RESULT_BUDGET_BYTES && kept.length === 1) {
697
+ // Measured with the longest cursor this page could carry.
698
+ const room = RESULT_BUDGET_BYTES - jsonBytes({ ...result, items: [] }) - 64;
699
+ kept[0] = fitItem(piece(start), room);
700
+ result = yield* build();
701
+ }
702
+ // A page must move forward: a continued grid that cannot fit even one
703
+ // row would hand back the cursor it was given.
704
+ const first = kept[0];
705
+ const stalled = first?.["moreQuestions"] !== undefined && asArray(first["questions"]).length === 0;
706
+ if (jsonBytes(result) > RESULT_BUDGET_BYTES || stalled) {
707
+ throw new ConnectorCallError("connector_call_failed", `Item ${start} of this form cannot be projected inside one result (${RESULT_BUDGET_BYTES} bytes), even alone; open the form in Forms.`, { retryable: false });
708
+ }
709
+ const safe = standardResult(result);
710
+ return {
711
+ ...selectFields(safe, args.fields),
712
+ page: result.page,
713
+ ...(safe["truncated"] ? { truncated: safe["truncated"] } : {}),
714
+ };
715
+ }).pipe(knownFailures),
716
+ }),
717
+ write(connection, {
718
+ name: "create_form",
719
+ description: "Create an empty Google Form with a title in the user's Drive. Google sets only the title here: add questions and a description with batch_update_form.",
720
+ input: Schema.Struct({
721
+ title: string("Title responders see.", 1000),
722
+ documentTitle: Schema.optionalKey(string("Drive file name; defaults to title.", 1000)),
723
+ unpublished: Schema.optionalKey(Schema.Boolean.annotate({
724
+ description: "Create without accepting responses; only the owner can publish it later. Omit for Google default publishing.",
725
+ })),
726
+ }),
727
+ output: optional({
728
+ formId: Schema.String,
729
+ title: Schema.String,
730
+ documentTitle: Schema.String,
731
+ revisionId: Schema.String,
732
+ responderUri: Schema.String,
733
+ editUrl: Schema.String,
734
+ }),
735
+ destructive: false,
736
+ run: (args) => Effect.gen(function* () {
737
+ const created = yield* client
738
+ .json({
739
+ method: "POST",
740
+ path: "/forms",
741
+ template: "/forms",
742
+ query: { unpublished: args["unpublished"] === true ? true : undefined },
743
+ body: { info: compact({ title: args["title"], documentTitle: args["documentTitle"] }) },
744
+ })
745
+ .pipe(Effect.catch((failure) => {
746
+ // Sent and not refused — no answer, a 5xx or redirect after Google received
747
+ // it, or a reply that broke: the form may exist, and there is no
748
+ // form id to re-read. Another create would make a second one, so
749
+ // say where to look first. The shared verdict (not retryable)
750
+ // stands: a create, a batch, and an info update are each not safe
751
+ // to send twice blind.
752
+ // Any phase that is neither "never sent" nor "explicitly refused"
753
+ // is uncertain, so a phase the shared layer adds later is too.
754
+ const outcome = googleOutcomeOf(failure);
755
+ return outcome?.dispatched === true && outcome.phase !== "refused" && outcome.phase !== "before-send"
756
+ ? googleFailureMessage(connection, failure, (message) => `${message} create_form makes a new form on every call: look for "${args["documentTitle"] ?? args["title"]}" in the user's Drive, or ask them, before creating it again.`)
757
+ : Effect.fail(failure);
758
+ }));
759
+ const form = asRecord(created);
760
+ const formId = text(form["formId"]);
761
+ const info = asRecord(form["info"]);
762
+ return compact({
763
+ formId,
764
+ title: text(info["title"]),
765
+ documentTitle: text(info["documentTitle"]),
766
+ revisionId: text(form["revisionId"]),
767
+ responderUri: text(form["responderUri"]),
768
+ editUrl: formId ? editUrl(formId) : undefined,
769
+ });
770
+ }).pipe(knownFailures),
771
+ }),
772
+ read(connection, {
773
+ name: "list_responses",
774
+ description: "List a Google Form's submitted responses, each answer labeled with its question's title in form order. Long answers are cut; get_response reads one whole.",
775
+ input: Schema.Struct({
776
+ fields: Schema.optionalKey(fieldsInput("Dot paths to keep in each response item; only narrows safe data.")),
777
+ formId,
778
+ submittedAfter: Schema.optionalKey(Schema.String.check(pattern("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?Z$")).annotate({
779
+ description: "Exclusive submitted-after UTC instant, e.g. 2026-10-01T00:00:00Z.",
780
+ })),
781
+ limit: Schema.optionalKey(Integer.check(Schema.isBetween({ minimum: 1, maximum: MAX_PAGE_SIZE })).annotate({
782
+ description: "Maximum responses; default 25, at most 100.",
783
+ default: 25,
784
+ })),
785
+ cursor,
786
+ }),
787
+ output: Schema.Struct({
788
+ items: Schema.Array(ResponseRecord),
789
+ page: StandardPage,
790
+ truncated: Schema.optionalKey(Truncated),
791
+ }),
792
+ writeSteps: pairedReads,
793
+ run: (args) => Effect.gen(function* () {
794
+ const ctx = yield* contextOf;
795
+ const problem = fieldsProblem(args.fields, new Set(Object.keys(ResponseRecord.fields)), "a response item");
796
+ if (problem)
797
+ return yield* Effect.fail(new Refusal({ error: problem }));
798
+ const formId = args["formId"];
799
+ const limit = args["limit"] ?? DEFAULT_PAGE_SIZE;
800
+ const digest = yield* Effect.promise(() => digestOf(["list_responses", formId, args["submittedAfter"] ?? null]));
801
+ // A cursor names Google's page (its token and size) and, when a page
802
+ // ended early to stay inside its byte budget, the last response it
803
+ // returned and a fingerprint of that Google page's ordered response
804
+ // ids. The next call re-reads the page and continues strictly after
805
+ // that response only if the page is exactly as it was: Google promises
806
+ // neither a stable order nor a snapshot, so a reordered, grown, or
807
+ // shrunk page could repeat, skip, or cycle, and is refused instead.
808
+ // Moving to Google's next page needs none of this: its token is Google's.
809
+ const resume = typeof args["cursor"] === "string"
810
+ ? decodeCursor(yield* boundCursor.decode("list_responses", { formId, submittedAfter: args["submittedAfter"] }, args["cursor"]), digest, ["d", "t", "a", "h", "n"], (c) => (c["t"] === null || typeof c["t"] === "string") &&
811
+ ((c["a"] === null && c["h"] === null) ||
812
+ (typeof c["a"] === "string" && c["a"] !== "" && typeof c["h"] === "string")) &&
813
+ isIndex(c["n"]) &&
814
+ c["n"] >= 1 &&
815
+ c["n"] <= MAX_PAGE_SIZE, "with the same formId and submittedAfter")
816
+ : undefined;
817
+ const token = resume?.["t"] ?? null;
818
+ const size = resume?.["n"] ?? limit;
819
+ const anchor = resume?.["a"] ?? null;
820
+ const fingerprint = resume?.["h"] ?? null;
821
+ // The form labels the answers. Both reads are one person's view, so
822
+ // either failing fails the call rather than returning bare ids.
823
+ const [form, listing] = yield* Effect.all([
824
+ readForm(formId, "form"),
825
+ client.json({
826
+ method: "GET",
827
+ path: `${formPath(formId)}/responses`,
828
+ template: "/forms/{formId}/responses",
829
+ query: {
830
+ filter: args["submittedAfter"] ? `timestamp > ${args["submittedAfter"]}` : undefined,
831
+ pageSize: size,
832
+ pageToken: token ?? undefined,
833
+ },
834
+ }, { step: "answers" }),
835
+ ], { concurrency: "unbounded" });
836
+ const labels = questionLabels(ctx, form);
837
+ const page = asRecord(listing);
838
+ const rows = asArray(page["responses"]);
839
+ const idOf = (row) => text(asRecord(row)["responseId"]);
840
+ const pageHash = yield* Effect.promise(() => digestOf(["page", ...rows.map((row) => idOf(row) ?? null)]));
841
+ // Each response id as a cursor names it.
842
+ const claims = yield* Effect.promise(() => Promise.all(rows.map((row) => claimOf(ctx, idOf(row) ?? ""))));
843
+ let start = 0;
844
+ if (anchor !== null) {
845
+ const at = claims.findIndex((claim, index) => idOf(rows[index]) !== undefined && claim === anchor);
846
+ if (fingerprint !== pageHash || at < 0) {
847
+ throw new ConnectorCallError("conflict", "Google's page of responses changed since this cursor was issued (reordered, or responses added or removed), so continuing could repeat or skip some. Start again without cursor, or pass the newest lastSubmittedTime already read as submittedAfter.");
848
+ }
849
+ start = at + 1;
850
+ }
851
+ const remedy = "get_response reads it whole";
852
+ const responses = [];
853
+ let used = jsonBytes({ formId, responses: [], page: { hasMore: true, nextCursor: null } }) + PAGE_RESERVE_BYTES;
854
+ for (let index = start; index < rows.length; index += 1) {
855
+ const room = RESULT_BUDGET_BYTES - used;
856
+ let response = projectResponse(ctx, rows[index], labels, LIST_ANSWER_CHARS, remedy);
857
+ if (jsonBytes(response) + 1 > room) {
858
+ if (responses.length > 0)
859
+ break;
860
+ response = fitResponse(ctx, rows[index], labels, [LIST_ANSWER_CHARS, 200], remedy, room - 1);
861
+ }
862
+ responses.push(response);
863
+ used += jsonBytes(response) + 1;
864
+ }
865
+ const rawToken = page["nextPageToken"];
866
+ if (rawToken !== undefined &&
867
+ (typeof rawToken !== "string" ||
868
+ (rawToken &&
869
+ (!rawToken.trim() ||
870
+ /^[a-z][a-z0-9+.-]*:/i.test(rawToken) ||
871
+ [...rawToken].some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127)))))
872
+ throw new ConnectorCallError("connector_call_failed", "Google returned malformed response paging metadata.");
873
+ const google = text(rawToken);
874
+ const googleNext = google === undefined ? null : redactSentSecrets(ctx, google);
875
+ if (googleNext && (googleNext !== google || googleNext.includes("[redacted]")))
876
+ throw new ConnectorCallError("connector_call_failed", "Google returned a withheld response page token; continuation is unavailable.");
877
+ const build = () => Effect.gen(function* () {
878
+ const last = start + responses.length - 1;
879
+ // Rows left on Google's page: continue after the last one returned.
880
+ // A page that ended on an anchor with nothing after it has no
881
+ // response of its own to anchor on, and moves to Google's next page.
882
+ let next;
883
+ if (start + responses.length < rows.length && responses.length > 0) {
884
+ const after = idOf(rows[last]) === undefined ? undefined : claims[last];
885
+ if (after === undefined) {
886
+ throw new ConnectorCallError("connector_call_failed", "Google returned a response without an id, so this page cannot be continued safely.", { retryable: false });
887
+ }
888
+ next = { d: digest, t: token, a: after, h: pageHash, n: size };
889
+ }
890
+ else if (googleNext) {
891
+ next = { d: digest, t: googleNext, a: null, h: null, n: limit };
892
+ }
893
+ const nextCursor = next
894
+ ? yield* encodeCursor(ctx, "list_responses", { formId, submittedAfter: args["submittedAfter"] }, next)
895
+ : undefined;
896
+ return {
897
+ items: responses,
898
+ page: { hasMore: nextCursor !== undefined, ...(nextCursor ? { nextCursor } : {}) },
899
+ };
900
+ });
901
+ let result = yield* build();
902
+ while (jsonBytes(result) > RESULT_BUDGET_BYTES && responses.length > 1) {
903
+ responses.pop();
904
+ result = yield* build();
905
+ }
906
+ if (jsonBytes(result) > RESULT_BUDGET_BYTES && responses.length === 1) {
907
+ responses[0] = fitResponse(ctx, rows[start], labels, [LIST_ANSWER_CHARS, 200], remedy, RESULT_BUDGET_BYTES - jsonBytes({ ...result, items: [] }) - 1);
908
+ result = yield* build();
909
+ }
910
+ if (jsonBytes(result) > RESULT_BUDGET_BYTES) {
911
+ throw new ConnectorCallError("connector_call_failed", "A response on this page cannot be listed inside one result, even cut; read it with get_response and questionIds.", { retryable: false });
912
+ }
913
+ const safe = standardResult(result);
914
+ return {
915
+ items: selectFields(result.items, args.fields),
916
+ page: result.page,
917
+ ...(safe["truncated"] ? { truncated: safe["truncated"] } : {}),
918
+ };
919
+ }).pipe(knownFailures),
920
+ }),
921
+ read(connection, {
922
+ name: "get_response",
923
+ description: "Get one Google Form response whole, each answer labeled with its question's title, plus file ids, quiz grades, and feedback. questionIds narrows a large one.",
924
+ input: Schema.Struct({
925
+ formId,
926
+ responseId: id("Response id from list_responses."),
927
+ questionIds: Schema.optionalKey(Schema.Array(id("Question id from get_form or omittedQuestionIds."))
928
+ .check(Schema.isMinLength(1), Schema.isMaxLength(MAX_QUESTION_IDS), Schema.isUnique())
929
+ .annotate({ description: "Return only these questions for a large response." })),
930
+ fields: Schema.optionalKey(fieldsInput("Dot paths to keep in the response; cut and recovery metadata is preserved.")),
931
+ }),
932
+ output: ResponseRecord,
933
+ writeSteps: pairedReads,
934
+ run: (args) => Effect.gen(function* () {
935
+ const ctx = yield* contextOf;
936
+ const problem = fieldsProblem(args.fields, new Set(Object.keys(ResponseRecord.fields)), "the response");
937
+ if (problem)
938
+ return yield* Effect.fail(new Refusal({ error: problem }));
939
+ const formId = args["formId"];
940
+ const [form, response] = yield* Effect.all([
941
+ readForm(formId, "form"),
942
+ client.json({
943
+ method: "GET",
944
+ path: `${formPath(formId)}/responses/${encodeURIComponent(args["responseId"])}`,
945
+ template: "/forms/{formId}/responses/{responseId}",
946
+ }, { step: "answers" }),
947
+ ], { concurrency: "unbounded" });
948
+ const wanted = Array.isArray(args["questionIds"]) ? new Set(args["questionIds"]) : undefined;
949
+ const record = asRecord(response);
950
+ const narrowed = wanted
951
+ ? {
952
+ ...record,
953
+ answers: Object.fromEntries(Object.entries(asRecord(record["answers"])).filter(([key, answer]) => wanted.has(text(asRecord(answer)["questionId"]) ?? key))),
954
+ }
955
+ : record;
956
+ // Whole when it fits; otherwise cut, then narrowed, with the ids left
957
+ // out named so a call with questionIds reads them.
958
+ const result = fitResponse(ctx, narrowed, questionLabels(ctx, form), [undefined, MAX_ANSWER_CHARS, LIST_ANSWER_CHARS], "more than one result carries; pass questionIds to read fewer answers, or read it in Forms", RESULT_BUDGET_BYTES);
959
+ if (jsonBytes(result) > RESULT_BUDGET_BYTES) {
960
+ throw new ConnectorCallError("invalid_args", "This response cannot be returned inside one result even with its answers cut; pass fewer questionIds.");
961
+ }
962
+ const safe = standardResult(result);
963
+ return {
964
+ ...selectFields(safe, args.fields),
965
+ ...(safe["truncated"] ? { truncated: safe["truncated"] } : {}),
966
+ ...(safe["omittedQuestionIds"] ? { omittedQuestionIds: safe["omittedQuestionIds"] } : {}),
967
+ };
968
+ }).pipe(knownFailures),
969
+ }),
970
+ write(connection, {
971
+ name: "update_form_info",
972
+ description: "Replace a Google Form's title, description, or both; what is omitted is kept. The old text is gone. Questions and settings need batch_update_form.",
973
+ input: Schema.Struct({
974
+ formId,
975
+ title: Schema.optionalKey(string("New title; omit to preserve.", 1000)),
976
+ description: Schema.optionalKey(Schema.String.check(isMaxChars(20000)).annotate({
977
+ description: "New description; empty clears, omission preserves.",
978
+ })),
979
+ requiredRevisionId: Schema.optionalKey(revision),
980
+ }),
981
+ output: optional({ formId: Schema.String, revisionId: Schema.String }),
982
+ destructive: true,
983
+ run: (args) => Effect.gen(function* () {
984
+ const fields = ["title", "description"].filter((field) => args[field] !== undefined);
985
+ if (fields.length === 0) {
986
+ throw new ConnectorCallError("invalid_args", "update_form_info needs a title, a description, or both.");
987
+ }
988
+ const { formId, revisionId } = yield* batch(args["formId"], [
989
+ {
990
+ updateFormInfo: {
991
+ info: compact({ title: args["title"], description: args["description"] }),
992
+ updateMask: fields.join(","),
993
+ },
994
+ },
995
+ ], args["requiredRevisionId"]);
996
+ return compact({ formId, revisionId });
997
+ }).pipe(knownFailures),
998
+ }),
999
+ write(connection, {
1000
+ name: "batch_update_form",
1001
+ description: "Apply Forms API batchUpdate requests to a Google Form, all or none: create, update, move, or delete items, and change info or quiz settings. The host may ask the user to confirm this write.",
1002
+ input: Schema.Struct({
1003
+ formId,
1004
+ requests: Schema.Array(Schema.Struct(Object.fromEntries(FORMS_REQUEST_KINDS.map((kind) => [
1005
+ kind,
1006
+ Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown).annotate({
1007
+ description: `Forms ${kind} request body; use forms_api_details for nested properties.`,
1008
+ })),
1009
+ ]))).check(Schema.makeFilter((value) => Object.keys(value).length === 1, {
1010
+ expected: "exactly one reviewed request kind",
1011
+ toJsonSchema: () => ({ minProperties: 1, maxProperties: 1 }),
1012
+ })))
1013
+ .check(Schema.isMinLength(1), Schema.isMaxLength(MAX_REQUESTS))
1014
+ .annotate({
1015
+ description: "1–100 reviewed Forms requests, each exactly one kind. Locations are item indexes; use forms_api_details for nested request contracts.",
1016
+ }),
1017
+ requiredRevisionId: revision,
1018
+ }),
1019
+ output: BatchRecord,
1020
+ destructive: true,
1021
+ run: (args) => batch(args["formId"], args["requests"], args["requiredRevisionId"]).pipe(knownFailures),
1022
+ }),
1023
+ ];
1024
+ }