@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
@@ -6,7 +6,7 @@ import { skill } from "./skill.generated.js";
6
6
  * (https://developers.google.com/workspace/forms/api/reference/rest).
7
7
  *
8
8
  * Whose forms. Access is a service account with domain-wide delegation
9
- * (`src/providers/_shared/google/workspace.ts`): deployment config maps the admitted
9
+ * (`src/providers/_shared/google/`): deployment config maps the admitted
10
10
  * identity to a Workspace address, and each call mints, or reuses, a token as
11
11
  * that user. The Forms API has no per-user path to confine beneath — a form id
12
12
  * is a Drive file id — so the token's subject is what decides what a request
@@ -25,1114 +25,15 @@ import { skill } from "./skill.generated.js";
25
25
  * call, and `pnpm run providers:check --provider forms` reports a touched
26
26
  * contract that moved or a method that stopped accepting the scopes below.
27
27
  */
28
- import { apiConnector as api, defined } from "../../connectors/api-connector.js";
29
- import { ConnectorCallError } from "../../errors.js";
30
- import { clampText, jsonBytes, RESULT_BUDGET_BYTES } from "../_shared/google/result-size.js";
31
- import { googleOutcomeOf, GOOGLE_WORKSPACE_OPTIONS, googleWorkspaceClient, workspaceConnection, workspaceConnector, } from "../_shared/google/workspace.js";
32
- import { asProviderFactory } from "../../provider.js";
33
- /**
34
- * The Forms API root. Every path the tools send sits beneath `/forms`; there
35
- * is no user segment, because a form is a Drive file the token's subject can
36
- * or cannot open.
37
- */
38
- export const FORMS_API_BASE_URL = "https://forms.googleapis.com/v1";
39
- /**
40
- * Exactly the scopes this connection requests, and exactly what the Admin
41
- * console's domain-wide delegation entry must list (comma-separated there).
42
- * `forms.body` reads and edits forms; responses are read-only.
43
- */
44
- export const FORMS_SCOPES = [
45
- "https://www.googleapis.com/auth/forms.body",
46
- "https://www.googleapis.com/auth/forms.responses.readonly",
47
- ];
48
- /** Google's own response page maximum is 5,000; a page an agent reads is not. */
49
- const MAX_PAGE_SIZE = 100;
50
- const DEFAULT_PAGE_SIZE = 25;
51
- /** A listed answer is cut here; get_response returns it whole. */
52
- const LIST_ANSWER_CHARS = 2_000;
53
- /** A form or item title or description is cut here in get_form's projection. */
54
- const DESCRIPTION_CHARS = 2_000;
55
- /** ...and bounded here in bytes, so escaped control characters cannot grow it. */
56
- const DESCRIPTION_BYTES = 8 * 1024;
57
- /** Options shown per choice question; a long dropdown says how many it hid. */
58
- const MAX_OPTIONS = 100;
59
- /** An option or grid row is cut here; raw: true reads it whole. */
60
- const MAX_LABEL_CHARS = 300;
61
- const MAX_LABEL_BYTES = 1024;
62
- /** One answer value in get_response, before it would crowd out the rest. */
63
- const MAX_ANSWER_CHARS = 50_000;
64
- /** Question ids one get_response call may name. */
65
- const MAX_QUESTION_IDS = 100;
66
- /** Requests in one batch_update_form call. */
67
- const MAX_REQUESTS = 100;
68
- /** A large form with embedded media metadata, or a page of long answers. */
69
- const FORMS_MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
70
- /**
71
- * Room a filling page keeps for its own fields and its cursor. Every result
72
- * stays inside the shared RESULT_BUDGET_BYTES (`./google/result-size.ts`), so
73
- * it crosses execute_code's host bridge and a direct call alike; a page ends
74
- * early, with a cursor, rather than grow past it, and the complete result,
75
- * cursor included, is measured before it is returned.
76
- */
77
- const PAGE_RESERVE_BYTES = 4 * 1024;
78
- function asRecord(value) {
79
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
80
- }
81
- function asArray(value) {
82
- return Array.isArray(value) ? value : [];
83
- }
84
- function compact(value) {
85
- return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
86
- }
87
- function text(value) {
88
- return typeof value === "string" && value !== "" ? value : undefined;
89
- }
90
- function integer(value) {
91
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
92
- }
93
- /** Cut text at `max` characters and say so in the text itself. */
94
- function capped(value, max, remedy) {
95
- if (value === undefined || value.length <= max)
96
- return value;
97
- // Never split a surrogate pair: a lone high surrogate before the marker
98
- // would make the text ill-formed.
99
- let cut = value.slice(0, max);
100
- const last = cut.charCodeAt(cut.length - 1);
101
- if (last >= 0xd800 && last <= 0xdbff)
102
- cut = cut.slice(0, -1);
103
- return `${cut}\n[… ${value.length - cut.length} more characters truncated; ${remedy}]`;
104
- }
105
- /**
106
- * Text cut at `chars` characters, then held to `bytes` of JSON — quotes and
107
- * escapes counted — so a run of control characters cannot outgrow its place.
108
- */
109
- function bounded(value, chars, bytes) {
110
- const cut = capped(value, chars, "pass raw: true to read it");
111
- if (cut === undefined || jsonBytes(cut) <= bytes)
112
- return cut;
113
- return clampText(value, bytes, (dropped) => `\n[… ${dropped} more characters truncated; pass raw: true to read it]`);
114
- }
115
- function editUrl(formId) {
116
- return `https://docs.google.com/forms/d/${encodeURIComponent(formId)}/edit`;
117
- }
118
- const encoder = new TextEncoder();
119
- function base64Url(bytes) {
120
- let binary = "";
121
- for (const byte of bytes)
122
- binary += String.fromCharCode(byte);
123
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
124
- }
125
- /** What a cursor is bound to: its tool and every argument that decides its pages. */
126
- async function digestOf(bound) {
127
- const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(JSON.stringify(bound))));
128
- return base64Url(hash.subarray(0, 16));
129
- }
130
- function encodeCursor(cursor) {
131
- return base64Url(encoder.encode(JSON.stringify(cursor)));
132
- }
133
- /**
134
- * A cursor this tool issued for these same arguments, or a refusal: one
135
- * carried to another form, filter, or tool would page through something else.
136
- */
137
- function decodeCursor(value, digest, keys, valid, again) {
138
- let cursor;
139
- try {
140
- const base64 = value.replace(/-/g, "+").replace(/_/g, "/");
141
- const binary = atob(base64 + "=".repeat((4 - (base64.length % 4)) % 4));
142
- cursor = asRecord(JSON.parse(new TextDecoder().decode(Uint8Array.from(binary, (c) => c.charCodeAt(0)))));
143
- }
144
- catch {
145
- cursor = {};
146
- }
147
- const shaped = Object.keys(cursor).length === keys.length && keys.every((key) => key in cursor);
148
- if (!shaped || cursor["d"] !== digest || !valid(cursor)) {
149
- throw new ConnectorCallError("invalid_args", `cursor is not one this tool returned for these arguments. Pass page.nextCursor back unchanged, ${again}.`);
150
- }
151
- return cursor;
152
- }
153
- function isIndex(value) {
154
- return typeof value === "number" && Number.isInteger(value) && value >= 0;
155
- }
156
- const CHOICE_TYPES = {
157
- RADIO: "radio",
158
- CHECKBOX: "checkbox",
159
- DROP_DOWN: "drop_down",
160
- };
161
- function choiceOptions(choice) {
162
- const all = asArray(choice["options"]).map(asRecord);
163
- const hasOther = all.some((option) => option["isOther"] === true);
164
- const values = all
165
- .filter((option) => option["isOther"] !== true)
166
- .map((option) => bounded(String(option["value"] ?? ""), MAX_LABEL_CHARS, MAX_LABEL_BYTES) ?? "");
167
- return compact({
168
- options: values.slice(0, MAX_OPTIONS),
169
- moreOptions: values.length > MAX_OPTIONS ? values.length - MAX_OPTIONS : undefined,
170
- hasOther: hasOther || undefined,
171
- });
172
- }
173
- /** One question's id, type, and the shape of what it accepts. */
174
- function projectQuestion(value, rowTitle) {
175
- const question = asRecord(value);
176
- const grading = asRecord(question["grading"]);
177
- const base = {
178
- questionId: text(question["questionId"]),
179
- rowTitle,
180
- required: question["required"] === true,
181
- points: integer(grading["pointValue"]),
182
- };
183
- if (question["choiceQuestion"]) {
184
- const choice = asRecord(question["choiceQuestion"]);
185
- return compact({ ...base, type: CHOICE_TYPES[String(choice["type"])] ?? "unknown", ...choiceOptions(choice) });
186
- }
187
- if (question["textQuestion"]) {
188
- return compact({
189
- ...base,
190
- type: asRecord(question["textQuestion"])["paragraph"] === true ? "paragraph" : "short_text",
191
- });
192
- }
193
- if (question["scaleQuestion"]) {
194
- const scale = asRecord(question["scaleQuestion"]);
195
- return compact({
196
- ...base,
197
- type: "scale",
198
- scale: compact({
199
- low: integer(scale["low"]),
200
- high: integer(scale["high"]),
201
- lowLabel: text(scale["lowLabel"]),
202
- highLabel: text(scale["highLabel"]),
203
- }),
204
- });
205
- }
206
- if (question["dateQuestion"]) {
207
- const date = asRecord(question["dateQuestion"]);
208
- return compact({
209
- ...base,
210
- type: "date",
211
- includesYear: date["includeYear"] === true,
212
- includesTime: date["includeTime"] === true,
213
- });
214
- }
215
- if (question["timeQuestion"]) {
216
- return compact({ ...base, type: "time", duration: asRecord(question["timeQuestion"])["duration"] === true });
217
- }
218
- if (question["ratingQuestion"]) {
219
- const rating = asRecord(question["ratingQuestion"]);
220
- return compact({ ...base, type: "rating", scale: compact({ low: 1, high: integer(rating["ratingScaleLevel"]) }) });
221
- }
222
- if (question["fileUploadQuestion"])
223
- return compact({ ...base, type: "file_upload" });
224
- if (question["rowQuestion"])
225
- return compact({ ...base, type: "grid_row" });
226
- return compact({ ...base, type: "unknown" });
227
- }
228
- /**
229
- * One item, with every question it asks under `questions`: one for a question
230
- * item, one per row for a grid, none for a section break or media. Answers are
231
- * keyed by question id, so this is the map a response is read through.
232
- */
233
- function projectItem(value) {
234
- const item = asRecord(value);
235
- const base = {
236
- itemId: text(item["itemId"]),
237
- title: bounded(text(item["title"]), DESCRIPTION_CHARS, DESCRIPTION_BYTES),
238
- description: bounded(text(item["description"]), DESCRIPTION_CHARS, DESCRIPTION_BYTES),
239
- };
240
- if (item["questionItem"]) {
241
- return compact({
242
- ...base,
243
- kind: "question",
244
- questions: [projectQuestion(asRecord(item["questionItem"])["question"])],
245
- });
246
- }
247
- if (item["questionGroupItem"]) {
248
- const group = asRecord(item["questionGroupItem"]);
249
- const grid = asRecord(group["grid"]);
250
- const columns = asRecord(grid["columns"]);
251
- return compact({
252
- ...base,
253
- kind: "question_group",
254
- // A grid's rows share its columns: the choices are stated once, here.
255
- gridType: group["grid"] ? (CHOICE_TYPES[String(columns["type"])] ?? "unknown") : undefined,
256
- ...(group["grid"] ? choiceOptions(columns) : {}),
257
- questions: asArray(group["questions"]).map((question) => projectQuestion(question, bounded(text(asRecord(asRecord(question)["rowQuestion"])["title"]), MAX_LABEL_CHARS, MAX_LABEL_BYTES))),
258
- });
259
- }
260
- const kind = item["pageBreakItem"]
261
- ? "page_break"
262
- : item["textItem"]
263
- ? "text"
264
- : item["imageItem"]
265
- ? "image"
266
- : item["videoItem"]
267
- ? "video"
268
- : "unknown";
269
- return compact({ ...base, kind });
270
- }
271
- function projectForm(value) {
272
- const form = asRecord(value);
273
- const info = asRecord(form["info"]);
274
- const settings = asRecord(form["settings"]);
275
- const publish = asRecord(asRecord(form["publishSettings"])["publishState"]);
276
- const formId = text(form["formId"]);
277
- const items = asArray(form["items"]).map(projectItem);
278
- return compact({
279
- formId,
280
- title: bounded(text(info["title"]), DESCRIPTION_CHARS, DESCRIPTION_BYTES),
281
- documentTitle: bounded(text(info["documentTitle"]), DESCRIPTION_CHARS, DESCRIPTION_BYTES),
282
- description: bounded(text(info["description"]), DESCRIPTION_CHARS, DESCRIPTION_BYTES),
283
- revisionId: text(form["revisionId"]),
284
- responderUri: text(form["responderUri"]),
285
- editUrl: formId ? editUrl(formId) : undefined,
286
- linkedSheetId: text(form["linkedSheetId"]),
287
- isQuiz: asRecord(settings["quizSettings"])["isQuiz"] === true,
288
- emailCollection: text(settings["emailCollectionType"]),
289
- // Absent on a legacy form, which has no publish settings at all.
290
- published: form["publishSettings"] ? publish["isPublished"] === true : undefined,
291
- acceptingResponses: form["publishSettings"] ? publish["isAcceptingResponses"] === true : undefined,
292
- itemCount: items.length,
293
- questionCount: items.reduce((count, item) => count + asArray(item["questions"]).length, 0),
294
- items,
295
- });
296
- }
297
- /**
298
- * An item cut to `budget` bytes. Its own text and every option and label are
299
- * already bounded (an item without questions stays under ~120 KiB), so only a
300
- * grid's rows can outgrow a page; the rows that fit are kept and the rest
301
- * counted in `moreQuestions`, and the page's cursor continues inside this
302
- * item from the first row it left out. The caller still measures what it
303
- * returns.
304
- */
305
- function fitItem(item, budget) {
306
- if (jsonBytes(item) <= budget)
307
- return item;
308
- const questions = asArray(item["questions"]);
309
- const kept = [];
310
- let used = jsonBytes({ ...item, questions: [], moreQuestions: questions.length });
311
- for (const question of questions) {
312
- const size = jsonBytes(question) + 1;
313
- if (used + size > budget)
314
- break;
315
- kept.push(question);
316
- used += size;
317
- }
318
- return { ...item, questions: kept, moreQuestions: questions.length - kept.length };
319
- }
320
- /**
321
- * An item's questions from `from` on, for a page that continues inside it.
322
- * Its description and options went out with its first part; the continuation
323
- * carries what places the rows — its id, kind, and title — and where they
324
- * start, in `questionsFrom`.
325
- */
326
- function continuedItem(item, from) {
327
- if (from === 0)
328
- return item;
329
- return compact({
330
- itemId: item["itemId"],
331
- kind: item["kind"],
332
- title: item["title"],
333
- questionsFrom: from,
334
- questions: asArray(item["questions"]).slice(from),
335
- });
336
- }
337
- /**
338
- * Every question id in the form, labeled the way a person reads it — the
339
- * item's title, and the row's for a grid — in the form's own order.
340
- */
341
- function questionLabels(form) {
342
- const labels = new Map();
343
- for (const value of asArray(asRecord(form)["items"])) {
344
- const item = projectItem(value);
345
- for (const question of asArray(item["questions"]).map(asRecord)) {
346
- const id = text(question["questionId"]);
347
- if (!id)
348
- continue;
349
- const title = question["rowTitle"] ? `${item["title"] ?? ""} [${question["rowTitle"]}]`.trim() : item["title"];
350
- labels.set(id, { title, order: labels.size });
351
- }
352
- }
353
- return labels;
354
- }
355
- /**
356
- * A grader's feedback on one answer: its text, and the links or videos it
357
- * points to, each as the address a person opens and the label they were shown.
358
- */
359
- function projectFeedback(value, maxChars, remedy) {
360
- if (!value || typeof value !== "object")
361
- return undefined;
362
- const feedback = asRecord(value);
363
- const links = asArray(feedback["material"]).map((entry) => {
364
- const material = asRecord(entry);
365
- const link = asRecord(material["link"]);
366
- const video = asRecord(material["video"]);
367
- return compact({
368
- uri: text(link["uri"]) ?? text(video["youtubeUri"]),
369
- displayText: text(link["displayText"]) ?? text(video["displayText"]),
370
- video: material["video"] ? true : undefined,
371
- });
372
- });
373
- const said = text(feedback["text"]);
374
- return compact({
375
- text: maxChars === undefined ? said : capped(said, maxChars, remedy),
376
- links: links.length > 0 ? links : undefined,
377
- });
378
- }
379
- function projectResponse(value, labels, maxChars, remedy) {
380
- const response = asRecord(value);
381
- const answers = Object.entries(asRecord(response["answers"])).map(([key, raw]) => {
382
- const answer = asRecord(raw);
383
- const questionId = text(answer["questionId"]) ?? key;
384
- const grade = asRecord(answer["grade"]);
385
- const values = asArray(asRecord(answer["textAnswers"])["answers"]).map((entry) => {
386
- const value = String(asRecord(entry)["value"] ?? "");
387
- return maxChars === undefined ? value : capped(value, maxChars, remedy);
388
- });
389
- const files = asArray(asRecord(answer["fileUploadAnswers"])["answers"]).map((entry) => {
390
- const file = asRecord(entry);
391
- return compact({
392
- fileId: text(file["fileId"]),
393
- fileName: text(file["fileName"]),
394
- mimeType: text(file["mimeType"]),
395
- });
396
- });
397
- return compact({
398
- questionId,
399
- // Undefined for a question deleted since this response was submitted.
400
- title: labels.get(questionId)?.title,
401
- values: files.length > 0 && values.length === 0 ? undefined : values,
402
- files: files.length > 0 ? files : undefined,
403
- score: typeof grade["score"] === "number" ? grade["score"] : undefined,
404
- correct: typeof grade["correct"] === "boolean" ? grade["correct"] : undefined,
405
- feedback: projectFeedback(grade["feedback"], maxChars, remedy),
406
- });
407
- });
408
- // The form's order, then anything the form no longer asks.
409
- const rank = (id) => labels.get(id)?.order ?? Number.MAX_SAFE_INTEGER;
410
- answers.sort((a, b) => rank(a.questionId) - rank(b.questionId));
411
- return compact({
412
- responseId: text(response["responseId"]),
413
- createTime: text(response["createTime"]),
414
- lastSubmittedTime: text(response["lastSubmittedTime"]),
415
- respondentEmail: text(response["respondentEmail"]),
416
- totalScore: typeof response["totalScore"] === "number" ? response["totalScore"] : undefined,
417
- answers,
418
- });
419
- }
420
- /**
421
- * A response projected inside `budget` bytes: answers cut at each of `caps`
422
- * in turn until it fits, and if even the last does not, answers left out from
423
- * the end and named in `omittedQuestionIds`, for get_response to read by id.
424
- */
425
- function fitResponse(value, labels, caps, remedy, budget) {
426
- let projected = {};
427
- for (const cap of caps) {
428
- projected = projectResponse(value, labels, cap, remedy);
429
- if (jsonBytes(projected) <= budget)
430
- return projected;
431
- }
432
- const answers = asArray(projected["answers"]).map(asRecord);
433
- const kept = [];
434
- let used = jsonBytes({
435
- ...projected,
436
- answers: [],
437
- omittedQuestionIds: answers.map((answer) => answer["questionId"]),
438
- });
439
- for (const answer of answers) {
440
- const size = jsonBytes(answer) + 1;
441
- if (used + size > budget)
442
- break;
443
- kept.push(answer);
444
- used += size;
445
- }
446
- return {
447
- ...projected,
448
- answers: kept,
449
- omittedQuestionIds: answers.slice(kept.length).map((answer) => answer["questionId"]),
450
- };
451
- }
452
- // --- Writing ----------------------------------------------------------------------
453
- /** What a batch reply says it made: the new item and its question ids. */
454
- function projectReplies(value) {
455
- return asArray(value).map((entry) => {
456
- const created = asRecord(asRecord(entry)["createItem"]);
457
- return asRecord(entry)["createItem"]
458
- ? compact({
459
- createdItemId: text(created["itemId"]),
460
- createdQuestionIds: asArray(created["questionId"]).filter((id) => typeof id === "string"),
461
- })
462
- : {};
463
- });
464
- }
465
- /**
466
- * A batch's reply, inside one result. The write has already applied by the
467
- * time this runs, so nothing here may read as failure or as nothing changed:
468
- * a reply too large to return whole — a grid created with thousands of rows
469
- * names every row's question id — drops the question ids first and keeps each
470
- * created item's id, and if even that cannot fit, keeps only the counts.
471
- * Either way `truncated` and `note` say the batch applied and where to read
472
- * what was left out.
473
- */
474
- function projectBatch(formId, value) {
475
- const result = asRecord(value);
476
- const revisionId = text(asRecord(result["writeControl"])["requiredRevisionId"]);
477
- const replies = projectReplies(result["replies"]);
478
- const whole = compact({ formId, revisionId, replies });
479
- if (jsonBytes(whole) <= RESULT_BUDGET_BYTES)
480
- return whole;
481
- 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.";
482
- const idsOnly = compact({
483
- formId,
484
- revisionId,
485
- replies: replies.map((reply) => compact({ createdItemId: reply["createdItemId"] })),
486
- truncated: true,
487
- note,
488
- });
489
- if (jsonBytes(idsOnly) <= RESULT_BUDGET_BYTES)
490
- return idsOnly;
491
- return compact({
492
- formId,
493
- revisionId: revisionId && jsonBytes(revisionId) <= 1024 ? revisionId : undefined,
494
- replies: [],
495
- replyCount: replies.length,
496
- createdItemCount: replies.filter((reply) => reply["createdItemId"] !== undefined).length,
497
- truncated: true,
498
- note: revisionId && jsonBytes(revisionId) <= 1024 ? note : `${note} Read its revisionId there too.`,
499
- });
500
- }
501
- // --- Schemas ----------------------------------------------------------------------
502
- function input(properties, required) {
503
- return { type: "object", properties, required, additionalProperties: false };
504
- }
505
- const ID_PATTERN = "^[A-Za-z0-9_-]+$";
506
- function idProperty(description) {
507
- return { type: "string", minLength: 1, maxLength: 256, pattern: ID_PATTERN, description };
508
- }
509
- const FORM_ID_PROPERTY = idProperty("Form id: the part after /forms/d/ in its edit URL, a Drive file id, or create_form's formId. Not the /forms/d/e/… responder link.");
510
- const REVISION_PROPERTY = {
511
- type: "string",
512
- minLength: 1,
513
- maxLength: 256,
514
- pattern: "^\\S+$",
515
- description: "revisionId from get_form, create_form, or the previous edit. Google refuses the edit if the form changed since; re-read it and retry.",
516
- };
517
- function cursorProperty(same) {
518
- return {
519
- type: "string",
520
- minLength: 1,
521
- maxLength: 4096,
522
- description: `Opaque page.nextCursor from the previous page. Pass it back unchanged with the same ${same}.`,
523
- };
524
- }
525
- const LIMIT_PROPERTY = {
526
- type: "integer",
527
- minimum: 1,
528
- maximum: MAX_PAGE_SIZE,
529
- description: `Responses per page, 1 to ${MAX_PAGE_SIZE}; defaults to ${DEFAULT_PAGE_SIZE}. Connecta's cap, far below Google's 5,000; page with the cursor.`,
530
- };
531
- const RFC3339_UTC = "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,9})?Z$";
532
- const PAGE_SCHEMA = {
533
- type: "object",
534
- properties: {
535
- hasMore: { type: "boolean" },
536
- nextCursor: { type: ["string", "null"] },
537
- },
538
- required: ["hasMore", "nextCursor"],
539
- };
540
- const FILE_SCHEMA = {
541
- type: "object",
542
- properties: {
543
- fileId: { type: "string" },
544
- fileName: { type: "string" },
545
- mimeType: { type: "string" },
546
- },
547
- };
548
- const FEEDBACK_SCHEMA = {
549
- type: "object",
550
- properties: {
551
- text: { type: "string" },
552
- links: {
553
- type: "array",
554
- items: {
555
- type: "object",
556
- properties: {
557
- uri: { type: "string" },
558
- displayText: { type: "string" },
559
- video: { type: "boolean" },
560
- },
561
- },
562
- },
563
- },
564
- };
565
- const RESPONSE_SCHEMA = {
566
- type: "object",
567
- properties: {
568
- responseId: { type: "string" },
569
- createTime: { type: "string" },
570
- lastSubmittedTime: { type: "string" },
571
- respondentEmail: { type: "string" },
572
- totalScore: { type: "number" },
573
- omittedQuestionIds: { type: "array", items: { type: "string" } },
574
- answers: {
575
- type: "array",
576
- items: {
577
- type: "object",
578
- properties: {
579
- questionId: { type: "string" },
580
- title: { type: "string" },
581
- values: { type: "array", items: { type: "string" } },
582
- files: { type: "array", items: FILE_SCHEMA },
583
- score: { type: "number" },
584
- correct: { type: "boolean" },
585
- feedback: FEEDBACK_SCHEMA,
586
- },
587
- },
588
- },
589
- },
590
- };
591
- /** A choice question's, or a grid's shared, options. */
592
- const CHOICE_PROPERTIES = {
593
- options: { type: "array", items: { type: "string" } },
594
- moreOptions: { type: "integer" },
595
- hasOther: { type: "boolean" },
596
- };
597
- const QUESTION_SCHEMA = {
598
- type: "object",
599
- properties: {
600
- questionId: { type: "string" },
601
- type: { type: "string" },
602
- rowTitle: { type: "string" },
603
- required: { type: "boolean" },
604
- points: { type: "integer" },
605
- ...CHOICE_PROPERTIES,
606
- scale: {
607
- type: "object",
608
- properties: {
609
- low: { type: "integer" },
610
- high: { type: "integer" },
611
- lowLabel: { type: "string" },
612
- highLabel: { type: "string" },
613
- },
614
- },
615
- includesYear: { type: "boolean" },
616
- includesTime: { type: "boolean" },
617
- duration: { type: "boolean" },
618
- },
619
- };
620
- /**
621
- * get_form's result. Only `formId` is required, because `raw: true` returns
622
- * Google's own Form resource instead, where ProtoJSON omits every empty field —
623
- * an empty form has no `items` at all. The projection always carries `items`.
624
- */
625
- const FORM_SCHEMA = {
626
- type: "object",
627
- description: "The projection; with raw: true, Google's Form resource (formId, info, settings, publishSettings, items, revisionId), empty fields omitted.",
628
- properties: {
629
- formId: { type: "string" },
630
- title: { type: "string" },
631
- documentTitle: { type: "string" },
632
- description: { type: "string" },
633
- revisionId: { type: "string" },
634
- responderUri: { type: "string" },
635
- editUrl: { type: "string" },
636
- linkedSheetId: { type: "string" },
637
- isQuiz: { type: "boolean" },
638
- emailCollection: { type: "string" },
639
- published: { type: "boolean" },
640
- acceptingResponses: { type: "boolean" },
641
- itemCount: { type: "integer" },
642
- questionCount: { type: "integer" },
643
- page: PAGE_SCHEMA,
644
- items: {
645
- type: "array",
646
- items: {
647
- type: "object",
648
- properties: {
649
- itemId: { type: "string" },
650
- kind: { type: "string" },
651
- title: { type: "string" },
652
- description: { type: "string" },
653
- gridType: { type: "string" },
654
- ...CHOICE_PROPERTIES,
655
- questions: { type: "array", items: QUESTION_SCHEMA },
656
- questionsFrom: { type: "integer" },
657
- moreQuestions: { type: "integer" },
658
- },
659
- },
660
- },
661
- },
662
- required: ["formId"],
663
- };
664
- /**
665
- * One Forms `Request`: exactly one of Google's six kinds, each shaped as
666
- * Google documents it. The kind is closed here; its body is Google's.
667
- */
668
- function requestKind(description) {
669
- return { type: "object", description };
670
- }
671
- const REQUEST_SCHEMA = {
672
- type: "object",
673
- minProperties: 1,
674
- maxProperties: 1,
675
- additionalProperties: false,
676
- properties: {
677
- createItem: requestKind("{ item, location: { index } }"),
678
- updateItem: requestKind("{ item, location: { index }, updateMask }"),
679
- deleteItem: requestKind("{ location: { index } }"),
680
- moveItem: requestKind("{ originalLocation, newLocation }"),
681
- updateFormInfo: requestKind("{ info: { title?, description? }, updateMask }"),
682
- updateSettings: requestKind("{ settings: { quizSettings?, emailCollectionType? }, updateMask }"),
683
- },
684
- };
685
- // --- Tools ------------------------------------------------------------------------
686
- function tools(client) {
687
- const readOnly = { readOnlyHint: true };
688
- const formPath = (formId) => `/forms/${encodeURIComponent(formId)}`;
689
- const readForm = (formId, ctx) => client.json({ method: "GET", path: formPath(formId) }, ctx);
690
- const batch = async (formId, requests, revisionId, ctx) => projectBatch(formId, await client.json({
691
- method: "POST",
692
- path: `${formPath(formId)}:batchUpdate`,
693
- body: compact({
694
- requests,
695
- writeControl: revisionId ? { requiredRevisionId: revisionId } : undefined,
696
- }),
697
- }, ctx,
698
- // A named revision makes Google's refused precondition mean "the form
699
- // moved on", which the shared client reports as `conflict`.
700
- { revisionGuarded: revisionId !== undefined }));
701
- return [
702
- {
703
- name: "get_form",
704
- 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.",
705
- annotations: readOnly,
706
- inputSchema: input({
707
- formId: FORM_ID_PROPERTY,
708
- raw: {
709
- type: "boolean",
710
- description: "Return Google's untouched Form resource (grading, images, section navigation) instead of the projection; read it before an updateItem that must keep them. Not paged: a form too large for one result is refused.",
711
- },
712
- cursor: cursorProperty("formId"),
713
- }, ["formId"]),
714
- outputSchema: FORM_SCHEMA,
715
- handler: async (args, ctx) => {
716
- const formId = args["formId"];
717
- if (args["raw"] === true && args["cursor"] !== undefined) {
718
- throw new ConnectorCallError("invalid_args", "raw: true is not paged; omit cursor.");
719
- }
720
- const form = await readForm(formId, ctx);
721
- if (args["raw"] === true) {
722
- const size = jsonBytes(form);
723
- if (size > RESULT_BUDGET_BYTES) {
724
- throw new ConnectorCallError("invalid_args", `This form's raw resource is ${size} bytes, more than one result carries (${RESULT_BUDGET_BYTES}). Read the projection, which pages its items with page.nextCursor, or edit with the item indexes it lists.`);
725
- }
726
- return form;
727
- }
728
- const { items, ...header } = projectForm(form);
729
- const all = asArray(items).map(asRecord);
730
- const revision = text(asRecord(form)["revisionId"]) ?? "";
731
- const digest = await digestOf(["get_form", formId]);
732
- // A cursor names an item and, inside a grid too large for one page,
733
- // the first of its questions not yet returned, so every question id
734
- // is reachable by paging, however many rows one grid has.
735
- let start = 0;
736
- let from = 0;
737
- if (typeof args["cursor"] === "string") {
738
- const cursor = decodeCursor(args["cursor"], digest, ["d", "i", "q", "r"], (c) => isIndex(c["i"]) && isIndex(c["q"]) && typeof c["r"] === "string", "with the same formId");
739
- // Items are addressed by index and rows by position, so a page of a
740
- // changed form would skip or repeat them without saying so.
741
- const at = all[cursor["i"]];
742
- if (cursor["r"] !== revision ||
743
- cursor["i"] > all.length ||
744
- (cursor["q"] > 0 && (at === undefined || cursor["q"] >= asArray(at["questions"]).length))) {
745
- throw new ConnectorCallError("conflict", "The form changed after the page this cursor continues. Read get_form again from the start, without cursor.");
746
- }
747
- start = cursor["i"];
748
- from = cursor["q"];
749
- }
750
- const piece = (index) => (index === start ? continuedItem(all[index], from) : all[index]);
751
- const kept = [];
752
- let used = jsonBytes({ ...header, items: [] }) + PAGE_RESERVE_BYTES;
753
- for (let index = start; index < all.length; index += 1) {
754
- let item = piece(index);
755
- let size = jsonBytes(item) + 1;
756
- if (used + size > RESULT_BUDGET_BYTES) {
757
- if (kept.length > 0)
758
- break;
759
- item = fitItem(item, RESULT_BUDGET_BYTES - used);
760
- size = jsonBytes(item) + 1;
761
- }
762
- kept.push(item);
763
- used += size;
764
- // A grid cut short ends the page: the cursor continues inside it.
765
- if (item["moreQuestions"])
766
- break;
767
- }
768
- const build = () => {
769
- const lastIndex = start + kept.length - 1;
770
- const last = kept[kept.length - 1];
771
- let next;
772
- if (last?.["moreQuestions"]) {
773
- const before = lastIndex === start ? from : 0;
774
- next = { d: digest, i: lastIndex, q: before + asArray(last["questions"]).length, r: revision };
775
- }
776
- else if (lastIndex + 1 < all.length) {
777
- next = { d: digest, i: lastIndex + 1, q: 0, r: revision };
778
- }
779
- const nextCursor = next ? encodeCursor(next) : null;
780
- return { ...header, page: { hasMore: nextCursor !== null, nextCursor }, items: kept };
781
- };
782
- // The estimate above reserved room; the complete result, cursor and
783
- // all, is what has to fit.
784
- let result = build();
785
- while (jsonBytes(result) > RESULT_BUDGET_BYTES && kept.length > 1) {
786
- kept.pop();
787
- result = build();
788
- }
789
- if (jsonBytes(result) > RESULT_BUDGET_BYTES && kept.length === 1) {
790
- // Measured with the longest cursor this page could carry.
791
- const room = RESULT_BUDGET_BYTES - jsonBytes({ ...result, items: [] }) - 64;
792
- kept[0] = fitItem(piece(start), room);
793
- result = build();
794
- }
795
- // A page must move forward: a continued grid that cannot fit even one
796
- // row would hand back the cursor it was given.
797
- const first = kept[0];
798
- const stalled = first?.["moreQuestions"] !== undefined && asArray(first["questions"]).length === 0;
799
- if (jsonBytes(result) > RESULT_BUDGET_BYTES || stalled) {
800
- 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 });
801
- }
802
- return result;
803
- },
804
- },
805
- {
806
- name: "create_form",
807
- 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.",
808
- // Additive: a new, empty form changes nothing that existed. Not
809
- // read-only: program calls require a writable endpoint; other writes use
810
- // call_destructive_tool. The host controls approval.
811
- annotations: { readOnlyHint: false, destructiveHint: false },
812
- inputSchema: input({
813
- title: {
814
- type: "string",
815
- minLength: 1,
816
- maxLength: 1_000,
817
- description: "Title responders see.",
818
- },
819
- documentTitle: {
820
- type: "string",
821
- minLength: 1,
822
- maxLength: 1_000,
823
- description: "File name in Drive; defaults to the title. It cannot be changed through this connection later.",
824
- },
825
- unpublished: {
826
- type: "boolean",
827
- description: "Create it not accepting responses. This connection cannot publish it later; the owner does, in Forms. Omit to publish.",
828
- },
829
- }, ["title"]),
830
- outputSchema: {
831
- type: "object",
832
- properties: {
833
- formId: { type: "string" },
834
- title: { type: "string" },
835
- documentTitle: { type: "string" },
836
- revisionId: { type: "string" },
837
- responderUri: { type: "string" },
838
- editUrl: { type: "string" },
839
- },
840
- required: ["formId"],
841
- },
842
- handler: async (args, ctx) => {
843
- let created;
844
- try {
845
- created = await client.json({
846
- method: "POST",
847
- path: "/forms",
848
- query: { unpublished: args["unpublished"] === true ? true : undefined },
849
- body: { info: compact({ title: args["title"], documentTitle: args["documentTitle"] }) },
850
- }, ctx);
851
- }
852
- catch (error) {
853
- // Sent and not refused — no answer, a 5xx or redirect after Google received
854
- // it, or a reply that broke: the form may exist, and there is no
855
- // form id to re-read. Another create would make a second one, so
856
- // say where to look first. The shared verdict (not retryable)
857
- // stands. No Forms write is sent as idempotent: a create, a batch,
858
- // and an info update are each not safe to send twice blind.
859
- // Any phase that is neither "never sent" nor "explicitly refused"
860
- // is uncertain, so a phase the shared layer adds later is too.
861
- const outcome = googleOutcomeOf(error);
862
- if (error instanceof ConnectorCallError &&
863
- outcome?.dispatched === true &&
864
- outcome.phase !== "refused" &&
865
- outcome.phase !== "before-send") {
866
- throw new ConnectorCallError(error.code, `${error.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.`, { retryable: false, cause: error });
867
- }
868
- throw error;
869
- }
870
- const form = asRecord(created);
871
- const formId = text(form["formId"]);
872
- const info = asRecord(form["info"]);
873
- return compact({
874
- formId,
875
- title: text(info["title"]),
876
- documentTitle: text(info["documentTitle"]),
877
- revisionId: text(form["revisionId"]),
878
- responderUri: text(form["responderUri"]),
879
- editUrl: formId ? editUrl(formId) : undefined,
880
- });
881
- },
882
- },
883
- {
884
- name: "list_responses",
885
- 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.",
886
- annotations: readOnly,
887
- inputSchema: input({
888
- formId: FORM_ID_PROPERTY,
889
- submittedAfter: {
890
- type: "string",
891
- pattern: RFC3339_UTC,
892
- description: "Only responses submitted after this UTC instant (exclusive), e.g. 2026-10-01T00:00:00Z or the newest lastSubmittedTime already read.",
893
- },
894
- limit: LIMIT_PROPERTY,
895
- cursor: cursorProperty("formId and submittedAfter"),
896
- }, ["formId"]),
897
- outputSchema: {
898
- type: "object",
899
- properties: {
900
- formId: { type: "string" },
901
- responses: { type: "array", items: RESPONSE_SCHEMA },
902
- page: PAGE_SCHEMA,
903
- },
904
- required: ["formId", "responses", "page"],
905
- },
906
- handler: async (args, ctx) => {
907
- const formId = args["formId"];
908
- const limit = args["limit"] ?? DEFAULT_PAGE_SIZE;
909
- const digest = await digestOf(["list_responses", formId, args["submittedAfter"] ?? null]);
910
- // A cursor names Google's page (its token and size) and, when a page
911
- // ended early to stay inside its byte budget, the last response it
912
- // returned and a fingerprint of that Google page's ordered response
913
- // ids. The next call re-reads the page and continues strictly after
914
- // that response only if the page is exactly as it was: Google promises
915
- // neither a stable order nor a snapshot, so a reordered, grown, or
916
- // shrunk page could repeat, skip, or cycle, and is refused instead.
917
- // Moving to Google's next page needs none of this: its token is Google's.
918
- const resume = typeof args["cursor"] === "string"
919
- ? decodeCursor(args["cursor"], digest, ["d", "t", "a", "h", "n"], (c) => (c["t"] === null || typeof c["t"] === "string") &&
920
- ((c["a"] === null && c["h"] === null) ||
921
- (typeof c["a"] === "string" && c["a"] !== "" && typeof c["h"] === "string")) &&
922
- isIndex(c["n"]) &&
923
- c["n"] >= 1 &&
924
- c["n"] <= MAX_PAGE_SIZE, "with the same formId and submittedAfter")
925
- : undefined;
926
- const token = resume?.["t"] ?? null;
927
- const size = resume?.["n"] ?? limit;
928
- const anchor = resume?.["a"] ?? null;
929
- const fingerprint = resume?.["h"] ?? null;
930
- // The form labels the answers. Both reads are one person's view, so
931
- // either failing fails the call rather than returning bare ids.
932
- const [form, listing] = await Promise.all([
933
- readForm(formId, ctx),
934
- client.json({
935
- method: "GET",
936
- path: `${formPath(formId)}/responses`,
937
- query: {
938
- filter: args["submittedAfter"] ? `timestamp > ${args["submittedAfter"]}` : undefined,
939
- pageSize: size,
940
- pageToken: token ?? undefined,
941
- },
942
- }, ctx),
943
- ]);
944
- const labels = questionLabels(form);
945
- const page = asRecord(listing);
946
- const rows = asArray(page["responses"]);
947
- const idOf = (row) => text(asRecord(row)["responseId"]);
948
- const pageHash = await digestOf(["page", ...rows.map((row) => idOf(row) ?? null)]);
949
- let start = 0;
950
- if (anchor !== null) {
951
- const at = rows.findIndex((row) => idOf(row) === anchor);
952
- if (fingerprint !== pageHash || at < 0) {
953
- 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.");
954
- }
955
- start = at + 1;
956
- }
957
- const remedy = "get_response reads it whole";
958
- const responses = [];
959
- let used = jsonBytes({ formId, responses: [], page: { hasMore: true, nextCursor: null } }) + PAGE_RESERVE_BYTES;
960
- for (let index = start; index < rows.length; index += 1) {
961
- const room = RESULT_BUDGET_BYTES - used;
962
- let response = projectResponse(rows[index], labels, LIST_ANSWER_CHARS, remedy);
963
- if (jsonBytes(response) + 1 > room) {
964
- if (responses.length > 0)
965
- break;
966
- response = fitResponse(rows[index], labels, [LIST_ANSWER_CHARS, 200], remedy, room - 1);
967
- }
968
- responses.push(response);
969
- used += jsonBytes(response) + 1;
970
- }
971
- const googleNext = text(page["nextPageToken"]) ?? null;
972
- const build = () => {
973
- const last = start + responses.length - 1;
974
- // Rows left on Google's page: continue after the last one returned.
975
- // A page that ended on an anchor with nothing after it has no
976
- // response of its own to anchor on, and moves to Google's next page.
977
- let next;
978
- if (start + responses.length < rows.length && responses.length > 0) {
979
- const after = idOf(rows[last]);
980
- if (after === undefined) {
981
- throw new ConnectorCallError("connector_call_failed", "Google returned a response without an id, so this page cannot be continued safely.", { retryable: false });
982
- }
983
- next = { d: digest, t: token, a: after, h: pageHash, n: size };
984
- }
985
- else if (googleNext) {
986
- next = { d: digest, t: googleNext, a: null, h: null, n: limit };
987
- }
988
- const nextCursor = next ? encodeCursor(next) : null;
989
- return { formId, responses, page: { hasMore: nextCursor !== null, nextCursor } };
990
- };
991
- let result = build();
992
- while (jsonBytes(result) > RESULT_BUDGET_BYTES && responses.length > 1) {
993
- responses.pop();
994
- result = build();
995
- }
996
- if (jsonBytes(result) > RESULT_BUDGET_BYTES && responses.length === 1) {
997
- responses[0] = fitResponse(rows[start], labels, [LIST_ANSWER_CHARS, 200], remedy, RESULT_BUDGET_BYTES - jsonBytes({ ...result, responses: [] }) - 1);
998
- result = build();
999
- }
1000
- if (jsonBytes(result) > RESULT_BUDGET_BYTES) {
1001
- 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 });
1002
- }
1003
- return result;
1004
- },
1005
- },
1006
- {
1007
- name: "get_response",
1008
- 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.",
1009
- annotations: readOnly,
1010
- inputSchema: input({
1011
- formId: FORM_ID_PROPERTY,
1012
- responseId: idProperty("Response id from list_responses."),
1013
- questionIds: {
1014
- type: "array",
1015
- minItems: 1,
1016
- maxItems: MAX_QUESTION_IDS,
1017
- uniqueItems: true,
1018
- items: idProperty("Question id from get_form, or from omittedQuestionIds."),
1019
- description: "Return only these questions' answers, for a response too large to read whole in one result.",
1020
- },
1021
- }, ["formId", "responseId"]),
1022
- outputSchema: { ...RESPONSE_SCHEMA, required: ["responseId", "answers"] },
1023
- handler: async (args, ctx) => {
1024
- const formId = args["formId"];
1025
- const [form, response] = await Promise.all([
1026
- readForm(formId, ctx),
1027
- client.json({ method: "GET", path: `${formPath(formId)}/responses/${encodeURIComponent(args["responseId"])}` }, ctx),
1028
- ]);
1029
- const wanted = Array.isArray(args["questionIds"]) ? new Set(args["questionIds"]) : undefined;
1030
- const record = asRecord(response);
1031
- const narrowed = wanted
1032
- ? {
1033
- ...record,
1034
- answers: Object.fromEntries(Object.entries(asRecord(record["answers"])).filter(([key, answer]) => wanted.has(text(asRecord(answer)["questionId"]) ?? key))),
1035
- }
1036
- : record;
1037
- // Whole when it fits; otherwise cut, then narrowed, with the ids left
1038
- // out named so a call with questionIds reads them.
1039
- const result = fitResponse(narrowed, questionLabels(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);
1040
- if (jsonBytes(result) > RESULT_BUDGET_BYTES) {
1041
- throw new ConnectorCallError("invalid_args", "This response cannot be returned inside one result even with its answers cut; pass fewer questionIds.");
1042
- }
1043
- return result;
1044
- },
1045
- },
1046
- {
1047
- name: "update_form_info",
1048
- 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.",
1049
- // Destructive: the title or description it replaces is not recoverable
1050
- // through this connection.
1051
- annotations: { readOnlyHint: false, destructiveHint: true },
1052
- inputSchema: input({
1053
- formId: FORM_ID_PROPERTY,
1054
- title: {
1055
- type: "string",
1056
- minLength: 1,
1057
- maxLength: 1_000,
1058
- description: "New title responders see; omit to keep it.",
1059
- },
1060
- description: {
1061
- type: "string",
1062
- maxLength: 20_000,
1063
- description: "New description; an empty string clears it. Omit to keep it.",
1064
- },
1065
- requiredRevisionId: { ...REVISION_PROPERTY, description: `Optional. ${REVISION_PROPERTY.description}` },
1066
- }, ["formId"]),
1067
- outputSchema: {
1068
- type: "object",
1069
- properties: { formId: { type: "string" }, revisionId: { type: "string" } },
1070
- required: ["formId"],
1071
- },
1072
- handler: async (args, ctx) => {
1073
- const fields = ["title", "description"].filter((field) => args[field] !== undefined);
1074
- if (fields.length === 0) {
1075
- throw new ConnectorCallError("invalid_args", "update_form_info needs a title, a description, or both.");
1076
- }
1077
- const { formId, revisionId } = await batch(args["formId"], [
1078
- {
1079
- updateFormInfo: {
1080
- info: compact({ title: args["title"], description: args["description"] }),
1081
- updateMask: fields.join(","),
1082
- },
1083
- },
1084
- ], args["requiredRevisionId"], ctx);
1085
- return compact({ formId, revisionId });
1086
- },
1087
- },
1088
- {
1089
- name: "batch_update_form",
1090
- 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. Always needs approval.",
1091
- // Destructive whatever it carries: a deleteItem or an updateItem loses
1092
- // what it replaces, and the requests are Google's to interpret.
1093
- annotations: { readOnlyHint: false, destructiveHint: true },
1094
- inputSchema: input({
1095
- formId: FORM_ID_PROPERTY,
1096
- requests: {
1097
- type: "array",
1098
- minItems: 1,
1099
- maxItems: MAX_REQUESTS,
1100
- items: REQUEST_SCHEMA,
1101
- description: `1 to ${MAX_REQUESTS} Forms API Request objects, each exactly one kind, applied in order as Google documents them. Locations are item indexes.`,
1102
- },
1103
- requiredRevisionId: REVISION_PROPERTY,
1104
- }, ["formId", "requests", "requiredRevisionId"]),
1105
- outputSchema: {
1106
- type: "object",
1107
- properties: {
1108
- formId: { type: "string" },
1109
- revisionId: { type: "string" },
1110
- replies: {
1111
- type: "array",
1112
- items: {
1113
- type: "object",
1114
- properties: {
1115
- createdItemId: { type: "string" },
1116
- createdQuestionIds: { type: "array", items: { type: "string" } },
1117
- },
1118
- },
1119
- },
1120
- truncated: { type: "boolean" },
1121
- note: { type: "string" },
1122
- replyCount: { type: "integer" },
1123
- createdItemCount: { type: "integer" },
1124
- },
1125
- required: ["formId", "replies"],
1126
- },
1127
- handler: (args, ctx) => batch(args["formId"], args["requests"], args["requiredRevisionId"], ctx),
1128
- },
1129
- ];
1130
- }
1131
- // --- Guide ------------------------------------------------------------------------
1132
- function usageGuide(purpose, instructions) {
1133
- const extra = instructions?.trim();
1134
- return `${skill.fragments.guide_0}${purpose}${skill.fragments.guide_1}${RESULT_BUDGET_BYTES / 1024}${skill.fragments.guide_2}${LIST_ANSWER_CHARS}${skill.fragments.guide_3}${extra ? `\n## ${skill.instructionsHeading}\n\n${extra}\n` : ""}`;
1135
- }
28
+ import { bearer } from "../../http/oauth.js";
29
+ import { defineApiProvider } from "../../http/provider.js";
30
+ import { FORMS_HATCH, formsBinding } from "./lib/hatch.js";
31
+ import { GOOGLE_GRANT_RENEWAL, googleGrant, tagWorkspace, workspaceOf } from "../_shared/google/kit.js";
32
+ import { GOOGLE_WORKSPACE_OPTIONS } from "../_shared/google/workspace.js";
33
+ import { errors, FORMS, FormsApi, probe, safety } from "./api.js";
34
+ import { FORMS_API_BASE_URL, FORMS_MAX_RESPONSE_BYTES } from "./lib/constants.js";
35
+ import { formsTools } from "./tools.js";
36
+ export { FORMS_API_BASE_URL, FORMS_SCOPES } from "./lib/constants.js";
1136
37
  // --- Construction -----------------------------------------------------------------
1137
38
  /**
1138
39
  * A maintained Google Forms connection acting as each signed-in Workspace
@@ -1171,47 +72,50 @@ function usageGuide(purpose, instructions) {
1171
72
  * host approves through call_destructive_tool or a program on a writable endpoint;
1172
73
  * `update_form_info` and `batch_update_form` are destructive.
1173
74
  */
1174
- export const forms = asProviderFactory({
75
+ export const forms = defineApiProvider({
1175
76
  name: "forms",
1176
77
  title: "Google Forms",
1177
- kind: "api",
1178
78
  readme: "Google Forms",
1179
79
  bundle: {
1180
- "baselineGzip": 26161,
1181
- "maxGzip": 86161,
1182
- "note": "./providers/forms starts at 26,161 B gzip (#684): the same class as ./providers/gmail — a hand-written api() surface over the shared Workspace delegation layer, with no OAuth client and no MCP SDK. The cap uses the existing baseline + 60,000 B policy.",
80
+ baselineGzip: 372442,
81
+ note: "./providers/forms step-2 round-2 snapshot measures 372,120 B gzip standalone before refreshing these bundle facts (#703), Node 26.12.0 with scripts/measure-google-index.mjs. The same-toolchain main f9bf5afb snapshot measures 353,598 B. This entry includes the shared Effect provider kit, paid once per deployment; standalone provider sizes are not additive. Provider entries are reported, not capped (#899). Combined Gmail/Docs/Forms merge snapshot over main 84fb4eee measures 372,171 B gzip before recording these facts, Node 22.23.3, esbuild 0.28.1, neutral Worker conditions and gzip level 9; no cap changes (#703). Combined Google merge snapshot over main db979604 measures 372,442 B gzip before recording these facts, Node 22.23.3, esbuild 0.28.1, neutral Worker conditions and gzip level 9; no cap changes (#703).",
1183
82
  },
1184
83
  skill,
1185
84
  options: GOOGLE_WORKSPACE_OPTIONS,
1186
- create: formsConnector,
1187
- });
1188
- function formsConnector(id, options) {
1189
- const connection = workspaceConnection("forms", options);
1190
- const client = googleWorkspaceClient({
1191
- provider: "Google Forms",
1192
- api: "Google Forms API",
1193
- baseUrl: options.baseUrl?.trim() || FORMS_API_BASE_URL,
1194
- scopes: FORMS_SCOPES,
1195
- maxResponseBytes: FORMS_MAX_RESPONSE_BYTES,
1196
- // A form is a Drive file: Google answers 404 alike for one that does not
1197
- // exist and one this user cannot open, so a 404 is not proof of absence.
1198
- notFound: "ambiguous",
1199
- connection,
1200
- });
1201
- return workspaceConnector(connection, api(id, {
1202
- ...defined({
1203
- callAdmission: options.callAdmission,
1204
- maxResultBytes: options.maxResultBytes,
85
+ description: (options) => `Google Forms as the signed-in Workspace user: read and edit forms, read responses — ${options.purpose}`,
86
+ vendor: FormsApi,
87
+ baseUrl: (options) => options.baseUrl?.trim() || FORMS_API_BASE_URL,
88
+ maxResponseBytes: FORMS_MAX_RESPONSE_BYTES,
89
+ // The delegated token: the subject resolves per call, and the service
90
+ // account mints as that user. The key was checked at construction.
91
+ grant: (options) => googleGrant(FORMS, workspaceOf("forms", options)),
92
+ grantRenewal: GOOGLE_GRANT_RENEWAL,
93
+ authenticate: bearer,
94
+ errors,
95
+ safety: () => safety,
96
+ probe,
97
+ guide: (options) => ({
98
+ context: [
99
+ `Purpose: ${options.purpose}`,
100
+ "Workspace delegation is configured by the deployment. Each call acts as the mapped Workspace user; no argument chooses a subject. Responses, respondent email and quiz answers are user data; no configurable sensitive-field set is declared.",
101
+ ...(options.readOnly ? ["This connection is read-only."] : []),
102
+ ],
103
+ instructions: options.instructions,
104
+ summary: "Each caller's own forms: questions by id, labeled responses, revision-checked batch edits.",
105
+ // Required: whose forms they are, that Drive lists them, and the
106
+ // revision-checked edit sequence are conventions no schema can carry.
107
+ required: true,
108
+ }),
109
+ idempotencyKey: false,
110
+ pageSize: { default: 25, max: 100 },
111
+ hatch: {
112
+ ...FORMS_HATCH,
113
+ binding: ({ connection }) => formsBinding(connection),
114
+ notes: () => ({
115
+ read: "Paths are relative to /v1. Full vendor JSON follows the same privacy and permanent value-safety boundary as named reads. Responses page with top-level cursor from page.nextCursor; never pass a raw pageToken. query.pageSize accepts integers from 1 to 5000; omit it for the default. fields selects response data; query.fields is Google's wire mask.",
116
+ write: "Only create and batchUpdate are admitted. Batch bodies require 1–100 requests, one reviewed kind each, and writeControl.requiredRevisionId from a current read. Publish settings and all watches are refused. No idempotency key or automatic replay.",
1205
117
  }),
1206
- title: options.title ?? "Google Forms",
1207
- description: `Google Forms as the signed-in Workspace user: read and edit forms, read responses — ${connection.purpose}`,
1208
- usageGuide: {
1209
- content: usageGuide(connection.purpose, options.instructions),
1210
- summary: "Each caller's own forms: questions by id, labeled responses, revision-checked batch edits.",
1211
- // Required: whose forms they are, that Drive lists them, and the
1212
- // revision-checked edit sequence are conventions no schema can carry.
1213
- required: true,
1214
- },
1215
- tools: tools(client),
1216
- }));
1217
- }
118
+ },
119
+ tools: formsTools,
120
+ tagConnector: tagWorkspace("forms"),
121
+ });